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 + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -37,10 +79,29 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; 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(); + var enumValues = new List(); foreach (var value in values) { @@ -366,18 +290,10 @@ private static EnumerableReader CreateValuesDataReader( switch (DbConnectionPlusConfiguration.Instance.EnumSerializationMode) { case EnumSerializationMode.Integers: - return new EnumerableReader( - enumValues, - typeof(Int32?), - Constants.SingleColumnTemporaryTableColumnName - ); + return new(enumValues, typeof(int?), Constants.SingleColumnTemporaryTableColumnName); case EnumSerializationMode.Strings: - return new EnumerableReader( - enumValues, - typeof(String), - Constants.SingleColumnTemporaryTableColumnName - ); + return new(enumValues, typeof(string), Constants.SingleColumnTemporaryTableColumnName); default: return ThrowHelper.ThrowInvalidEnumSerializationModeException( @@ -386,10 +302,10 @@ private static EnumerableReader CreateValuesDataReader( } } - 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.SerializeEnums | EnumerableReaderOptions.ReadCharsAsStrings @@ -402,7 +318,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, MySqlConnection connection, MySqlTransaction? transaction) + private static void DropTemporaryTable(string name, MySqlConnection connection, MySqlTransaction? transaction) { using var command = connection.CreateCommand(); @@ -422,7 +338,7 @@ private static void DropTemporaryTable(String name, MySqlConnection connection, /// The transaction within to drop the table. /// A task representing the asynchronous operation. private static async ValueTask DropTemporaryTableAsync( - String name, + string name, MySqlConnection connection, MySqlTransaction? transaction ) @@ -439,5 +355,83 @@ private static async ValueTask DropTemporaryTableAsync( await command.ExecuteNonQueryAsync().ConfigureAwait(false); } - private readonly MySqlDatabaseAdapter 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 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(); + } } diff --git a/src/DbConnectionPlus.DatabaseAdapters.Oracle/GlobalUsings.cs b/src/DbConnectionPlus.DatabaseAdapters.Oracle/GlobalUsings.cs index cfc88dd..61f5ddc 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.Oracle/GlobalUsings.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.Oracle/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.Oracle/OracleConfigurationExtensions.cs b/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleConfigurationExtensions.cs index 3d9767e..9c548ea 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleConfigurationExtensions.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleConfigurationExtensions.cs @@ -2,7 +2,9 @@ using RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; #pragma warning disable IDE0130 +// ReSharper disable once CheckNamespace namespace RentADeveloper.DbConnectionPlus.Configuration; + #pragma warning restore IDE0130 /// diff --git a/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleDatabaseAdapter.cs b/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleDatabaseAdapter.cs index a1edfbc..9172a5c 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleDatabaseAdapter.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleDatabaseAdapter.cs @@ -11,6 +11,52 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; /// public class OracleDatabaseAdapter : IDatabaseAdapter { + private static readonly Dictionary typeToDbType = new() + { + { typeof(bool), DbType.Boolean }, + { typeof(byte), DbType.Byte }, + { typeof(byte[]), DbType.Binary }, + { typeof(char), DbType.StringFixedLength }, + { typeof(DateOnly), DbType.Date }, + { typeof(DateTime), DbType.DateTime }, + { typeof(DateTimeOffset), DbType.DateTimeOffset }, + { typeof(decimal), DbType.Decimal }, + { typeof(double), DbType.Double }, + { typeof(Guid), DbType.Guid }, + { typeof(short), DbType.Int16 }, + { typeof(int), DbType.Int32 }, + { typeof(long), DbType.Int64 }, + { typeof(float), DbType.Single }, + { typeof(string), DbType.String }, + { typeof(TimeOnly), DbType.Time }, + { typeof(TimeSpan), DbType.Time }, + }; + + private static readonly Dictionary typeToOracleDataType = new() + { + { typeof(bool), "NUMBER(1)" }, + { typeof(byte), "NUMBER(3)" }, + { typeof(byte[]), "RAW(2000)" }, + { typeof(char), "CHAR(1)" }, + { typeof(DateOnly), "DATE" }, + { typeof(DateTime), "TIMESTAMP" }, + { typeof(DateTimeOffset), "TIMESTAMP WITH TIME ZONE" }, + { typeof(decimal), "NUMBER(28,10)" }, + { typeof(double), "BINARY_DOUBLE" }, + { typeof(Guid), "RAW(16)" }, + { typeof(short), "NUMBER(5)" }, + { typeof(int), "NUMBER(10)" }, + { typeof(long), "NUMBER(19)" }, + { typeof(float), "BINARY_FLOAT" }, + { typeof(string), "NVARCHAR2(2000)" }, + { typeof(TimeOnly), "INTERVAL DAY TO SECOND" }, + { typeof(TimeSpan), "INTERVAL DAY TO SECOND" }, + }; + + private readonly OracleEntityManipulator entityManipulator; + private readonly ConcurrentDictionary supportsTemporaryTablesPerConnectionString = []; + private readonly OracleTemporaryTableBuilder temporaryTableBuilder; + /// /// Initializes a new instance of the class. /// @@ -20,6 +66,34 @@ public OracleDatabaseAdapter() this.temporaryTableBuilder = new(this); } + /// + /// + /// Determines whether the temporary tables feature of DbConnectionPlus + /// () is allowed to be used with Oracle databases. + /// Disabled by default. + /// + /// + /// WARNING: + /// Before enabling this feature, read the following note: + /// When using the temporary tables feature of DbConnectionPlus with an Oracle database, please be aware of the + /// following implications: + /// The temporary tables feature of DbConnectionPlus creates private temporary tables and drops them after use. + /// Unfortunately DDL statements (like creating and dropping a private temporary table) cause an implicit commit of + /// the current transaction in an Oracle database. + /// That means if you use the temporary tables feature inside an explicit transaction, the transaction will be + /// committed when the temporary table is created and again when it is dropped! + /// + /// + /// Therefore, when using DbConnectionPlus with Oracle databases, avoid using the temporary tables feature inside + /// explicit transactions or at least be aware of the implications. + /// You have been warned! + /// + /// + /// + /// If set to , attempting to use the temporary tables feature will throw an exception. + /// + public static bool AllowTemporaryTables { get; set; } + /// public IEntityManipulator EntityManipulator => this.entityManipulator; @@ -38,7 +112,7 @@ public ITemporaryTableBuilder TemporaryTableBuilder } /// - public void BindParameterValue(DbParameter parameter, Object? value) + public void BindParameterValue(DbParameter parameter, object? value) { ArgumentNullException.ThrowIfNull(parameter); @@ -57,16 +131,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( @@ -75,7 +146,7 @@ public void BindParameterValue(DbParameter parameter, Object? value) ); break; - case Byte[]: + case byte[]: parameter.DbType = DbType.Binary; parameter.Value = value; break; @@ -98,11 +169,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); @@ -113,14 +183,11 @@ public String GetDataType(Type type, EnumSerializationMode enumSerializationMode { return enumSerializationMode switch { - EnumSerializationMode.Strings => - "NVARCHAR2(200)", // 200 should be enough for most enum names + EnumSerializationMode.Strings => "NVARCHAR2(200)", // 200 should be enough for most enum names - EnumSerializationMode.Integers => - "NUMBER(10)", + EnumSerializationMode.Integers => "NUMBER(10)", - _ => - ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode) + _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode), }; } @@ -174,14 +241,11 @@ public DbType GetDbType(Type type, EnumSerializationMode enumSerializationMode) { return enumSerializationMode switch { - EnumSerializationMode.Strings => - DbType.String, + EnumSerializationMode.Strings => DbType.String, - EnumSerializationMode.Integers => - DbType.Int32, + EnumSerializationMode.Integers => DbType.Int32, - _ => - ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode) + _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode), }; } @@ -198,13 +262,12 @@ public DbType GetDbType(Type type, EnumSerializationMode enumSerializationMode) } /// - public String QuoteIdentifier(String identifier) => - "\"" + identifier + "\""; + public string QuoteIdentifier(string identifier) => "\"" + identifier + "\""; /// - public String QuoteTemporaryTableName(String tableName, DbConnection connection) + public string QuoteTemporaryTableName(string tableName, DbConnection connection) { - var prefix = connection.ExecuteScalar( + var prefix = connection.ExecuteScalar( "SELECT VALUE FROM v$parameter WHERE NAME = 'private_temp_table_prefix'" ); @@ -212,7 +275,7 @@ public String QuoteTemporaryTableName(String tableName, DbConnection connection) } /// - public Boolean SupportsTemporaryTables(DbConnection connection) + public bool SupportsTemporaryTables(DbConnection connection) { ArgumentNullException.ThrowIfNull(connection); @@ -224,10 +287,7 @@ public Boolean SupportsTemporaryTables(DbConnection connection) } /// - public Boolean WasSqlStatementCancelledByCancellationToken( - Exception exception, - CancellationToken cancellationToken - ) + public bool WasSqlStatementCancelledByCancellationToken(Exception exception, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(exception); @@ -252,34 +312,6 @@ CancellationToken cancellationToken return false; } - /// - /// - /// Determines whether the temporary tables feature of DbConnectionPlus - /// () is allowed to be used with Oracle databases. - /// Disabled by default. - /// - /// - /// WARNING: - /// Before enabling this feature, read the following note: - /// When using the temporary tables feature of DbConnectionPlus with an Oracle database, please be aware of the - /// following implications: - /// The temporary tables feature of DbConnectionPlus creates private temporary tables and drops them after use. - /// Unfortunately DDL statements (like creating and dropping a private temporary table) cause an implicit commit of - /// the current transaction in an Oracle database. - /// That means if you use the temporary tables feature inside an explicit transaction, the transaction will be - /// committed when the temporary table is created and again when it is dropped! - /// - /// - /// Therefore, when using DbConnectionPlus with Oracle databases, avoid using the temporary tables feature inside - /// explicit transactions or at least be aware of the implications. - /// You have been warned! - /// - /// - /// - /// If set to , attempting to use the temporary tables feature will throw an exception. - /// - public static Boolean AllowTemporaryTables { get; set; } - /// /// Throws an indicating that the temporary tables feature of /// DbConnectionPlus is disabled for Oracle databases. @@ -287,55 +319,9 @@ CancellationToken cancellationToken /// Always thrown. internal static void ThrowTemporaryTablesFeatureIsDisabledException() => throw new InvalidOperationException( - "The temporary tables feature of DbConnectionPlus is currently disabled for Oracle databases. " + - $"To enable it set {typeof(OracleDatabaseAdapter)}.{nameof(AllowTemporaryTables)} " + - "to true, but be sure to read the documentation first, because enabling this feature has implications " + - "for transaction management." + "The temporary tables feature of DbConnectionPlus is currently disabled for Oracle databases. " + + $"To enable it set {typeof(OracleDatabaseAdapter)}.{nameof(AllowTemporaryTables)} " + + "to true, but be sure to read the documentation first, because enabling this feature has implications " + + "for transaction management." ); - - private readonly OracleEntityManipulator entityManipulator; - private readonly ConcurrentDictionary supportsTemporaryTablesPerConnectionString = []; - private readonly OracleTemporaryTableBuilder temporaryTableBuilder; - - private static readonly Dictionary typeToDbType = new() - { - { typeof(Boolean), DbType.Boolean }, - { typeof(Byte), DbType.Byte }, - { typeof(Byte[]), DbType.Binary }, - { typeof(Char), DbType.StringFixedLength }, - { typeof(DateOnly), DbType.Date }, - { typeof(DateTime), DbType.DateTime }, - { typeof(DateTimeOffset), DbType.DateTimeOffset }, - { typeof(Decimal), DbType.Decimal }, - { typeof(Double), DbType.Double }, - { typeof(Guid), DbType.Guid }, - { typeof(Int16), DbType.Int16 }, - { typeof(Int32), DbType.Int32 }, - { typeof(Int64), DbType.Int64 }, - { typeof(Single), DbType.Single }, - { typeof(String), DbType.String }, - { typeof(TimeOnly), DbType.Time }, - { typeof(TimeSpan), DbType.Time } - }; - - private static readonly Dictionary typeToOracleDataType = new() - { - { typeof(Boolean), "NUMBER(1)" }, - { typeof(Byte), "NUMBER(3)" }, - { typeof(Byte[]), "RAW(2000)" }, - { typeof(Char), "CHAR(1)" }, - { typeof(DateOnly), "DATE" }, - { typeof(DateTime), "TIMESTAMP" }, - { typeof(DateTimeOffset), "TIMESTAMP WITH TIME ZONE" }, - { typeof(Decimal), "NUMBER(28,10)" }, - { typeof(Double), "BINARY_DOUBLE" }, - { typeof(Guid), "RAW(16)" }, - { typeof(Int16), "NUMBER(5)" }, - { typeof(Int32), "NUMBER(10)" }, - { typeof(Int64), "NUMBER(19)" }, - { typeof(Single), "BINARY_FLOAT" }, - { typeof(String), "NVARCHAR2(2000)" }, - { typeof(TimeOnly), "INTERVAL DAY TO SECOND" }, - { typeof(TimeSpan), "INTERVAL DAY TO SECOND" } - }; } diff --git a/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleEntityManipulator.cs b/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleEntityManipulator.cs index 3210d8f..33d34b9 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleEntityManipulator.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleEntityManipulator.cs @@ -4,26 +4,22 @@ using LinkDotNet.StringBuilder; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; -using RentADeveloper.DbConnectionPlus.Entities; namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; /// -/// The entity manipulator for PostgreSQL. +/// The entity manipulator for Oracle. /// -internal class OracleEntityManipulator : IEntityManipulator +/// The database adapter to use to manipulate entities. +internal class OracleEntityManipulator(OracleDatabaseAdapter databaseAdapter) : IEntityManipulator { - /// - /// Initializes a new instance of the class. - /// - /// The database adapter to use to manipulate entities. - public OracleEntityManipulator(OracleDatabaseAdapter databaseAdapter) => - this.databaseAdapter = databaseAdapter; + private readonly OracleDatabaseAdapter 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 UpdateDatabaseGeneratedProperties(entityTypeMetadata, outputParameters, entity); } } - 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) @@ -347,17 +313,17 @@ CancellationToken cancellationToken DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - totalNumberOfAffectedRows += - await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + totalNumberOfAffectedRows += await command + .ExecuteNonQueryAsync(cancellationToken) + .ConfigureAwait(false); var outputParameters = parameters.Where(a => a.Direction == ParameterDirection.Output).ToArray(); UpdateDatabaseGeneratedProperties(entityTypeMetadata, outputParameters, entity); } } - catch (Exception exception) when ( - this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) - ) + catch (Exception exception) + when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)) { throw new OperationCanceledException(cancellationToken); } @@ -367,9 +333,7 @@ CancellationToken cancellationToken } /// - public Int32 InsertEntity< - [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity - >( + public int InsertEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>( DbConnection connection, TEntity entity, DbTransaction? transaction, @@ -381,11 +345,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) @@ -405,9 +365,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); } @@ -415,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, @@ -429,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,9 +406,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); } @@ -463,9 +415,7 @@ CancellationToken cancellationToken } /// - public Int32 UpdateEntities< - [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity - >( + public int UpdateEntities<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>( DbConnection connection, IEnumerable entities, DbTransaction? transaction, @@ -477,11 +427,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) @@ -520,9 +466,8 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, outputParameters, entity); } } - catch (Exception exception) when ( - this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) - ) + catch (Exception exception) + when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)) { throw new OperationCanceledException(cancellationToken); } @@ -532,9 +477,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, @@ -546,11 +489,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) @@ -571,8 +510,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) { @@ -590,9 +530,8 @@ CancellationToken cancellationToken UpdateDatabaseGeneratedProperties(entityTypeMetadata, outputParameters, entity); } } - catch (Exception exception) when ( - this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) - ) + catch (Exception exception) + when (this.databaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken)) { throw new OperationCanceledException(cancellationToken); } @@ -602,9 +541,7 @@ CancellationToken cancellationToken } /// - public Int32 UpdateEntity< - [DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity - >( + public int UpdateEntity<[DynamicallyAccessedMembers(EntityHelper.EntityMemberTypes)] TEntity>( DbConnection connection, TEntity entity, DbTransaction? transaction, @@ -616,11 +553,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) @@ -649,9 +582,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); } @@ -659,9 +591,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, @@ -673,11 +603,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) @@ -706,15 +632,45 @@ 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); } } } + /// + /// Updates the database generated properties of the provided entity from the provided output parameters. + /// + /// The metadata for the entity type. + /// The output parameters from which to read the values for the properties. + /// The entity to update. + private static void UpdateDatabaseGeneratedProperties( + EntityTypeMetadata entityTypeMetadata, + DbParameter[] outputParameters, + object entity + ) + { + if (entityTypeMetadata.DatabaseGeneratedProperties.Count > 0) + { + for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++) + { + var property = entityTypeMetadata.DatabaseGeneratedProperties[i]; + if (!property.CanWrite) + { + continue; + } + + var value = outputParameters[i].Value; + + value = ValueConverter.ConvertValueToType(value, property.PropertyType); + + property.PropertySetter!(entity, value); + } + } + } + /// /// Creates a command to delete an entity. /// @@ -740,8 +696,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) @@ -801,7 +757,7 @@ EntityTypeMetadata entityTypeMetadata parameter.Direction = ParameterDirection.Output; - if (property.PropertyType == typeof(Byte[])) + if (property.PropertyType == typeof(byte[])) { // Use max size for byte arrays to actually retrieve the full value: parameter.Size = 32767; @@ -839,8 +795,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 entityTypeMetadata.UpdateProperties.Concat(whereProperties)) @@ -864,7 +820,7 @@ EntityTypeMetadata entityTypeMetadata parameter.Direction = ParameterDirection.Output; - if (property.PropertyType == typeof(Byte[])) + if (property.PropertyType == typeof(byte[])) { // Use max size for byte arrays to actually retrieve the full value: parameter.Size = 32767; @@ -882,7 +838,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, _ => @@ -892,7 +848,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"); @@ -907,8 +863,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) @@ -938,12 +894,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); @@ -1044,7 +1000,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, _ => @@ -1054,7 +1010,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"); sqlBuilder.Append(Constants.Indent); @@ -1091,8 +1047,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) @@ -1172,7 +1128,7 @@ private String GetUpdateEntitySqlCode(EntityTypeMetadata entityTypeMetadata) => private void PopulateParametersFromEntityProperties( EntityTypeMetadata entityTypeMetadata, List parameters, - Object entity + object entity ) { ArgumentNullException.ThrowIfNull(parameters); @@ -1185,40 +1141,4 @@ Object entity this.databaseAdapter.BindParameterValue(parameter, propertyValue); } } - - /// - /// Updates the database generated properties of the provided entity from the provided output parameters. - /// - /// The metadata for the entity type. - /// The output parameters from which to read the values for the properties. - /// The entity to update. - private static void UpdateDatabaseGeneratedProperties( - EntityTypeMetadata entityTypeMetadata, - DbParameter[] outputParameters, - Object entity - ) - { - if (entityTypeMetadata.DatabaseGeneratedProperties.Count > 0) - { - for (var i = 0; i < entityTypeMetadata.DatabaseGeneratedProperties.Count; i++) - { - var property = entityTypeMetadata.DatabaseGeneratedProperties[i]; - if (!property.CanWrite) - { - continue; - } - - var value = outputParameters[i].Value; - - value = ValueConverter.ConvertValueToType(value, property.PropertyType); - - property.PropertySetter!(entity, value); - } - } - } - - private readonly OracleDatabaseAdapter databaseAdapter; - private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new(); - private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new(); - private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new(); } diff --git a/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleTemporaryTableBuilder.cs b/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleTemporaryTableBuilder.cs index 8df49e7..fd7c034 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.Oracle/OracleTemporaryTableBuilder.cs @@ -4,7 +4,6 @@ using LinkDotNet.StringBuilder; using Oracle.ManagedDataAccess.Client; using RentADeveloper.DbConnectionPlus.DbCommands; -using RentADeveloper.DbConnectionPlus.Entities; using RentADeveloper.DbConnectionPlus.Extensions; using RentADeveloper.DbConnectionPlus.Readers; @@ -15,6 +14,8 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.Oracle; /// internal class OracleTemporaryTableBuilder : ITemporaryTableBuilder { + private readonly OracleDatabaseAdapter databaseAdapter; + /// /// Initializes a new instance of the class. /// @@ -36,10 +37,9 @@ public OracleTemporaryTableBuilder(OracleDatabaseAdapter 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 ) { @@ -80,8 +80,10 @@ public TemporaryTableDisposer BuildTemporaryTable( ); createCommand.Transaction = transaction; - using var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); + using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + createCommand, + cancellationToken + ); DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []); @@ -98,8 +100,10 @@ public TemporaryTableDisposer BuildTemporaryTable( ); createCommand.Transaction = transaction; - using var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); + using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + createCommand, + cancellationToken + ); DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []); @@ -131,10 +135,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 ) { @@ -177,8 +180,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, []); @@ -197,8 +201,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, []); @@ -226,6 +231,154 @@ await this.PopulateTemporaryTableAsync( ); } + /// + /// Builds an SQL code to insert data from the specified data reader into the specified temporary table. + /// + /// The quoted name of the table to insert data into. + /// 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, OracleParameter[] Parameters) BuildInsertSqlCode( + string quotedTableName, + [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType, + DbDataReader dataReader + ) + { + using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]); + + sqlBuilder.Append("INSERT INTO "); + sqlBuilder.AppendLine(quotedTableName); + + sqlBuilder.Append(Constants.Indent); + sqlBuilder.Append("("); + + var fieldCount = dataReader.FieldCount; + var parameters = new OracleParameter[fieldCount]; + + if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) + { + sqlBuilder.Append("\""); + sqlBuilder.Append(Constants.SingleColumnTemporaryTableColumnName); + sqlBuilder.Append("\""); + + parameters[0] = new() { ParameterName = Constants.SingleColumnTemporaryTableColumnName }; + } + else + { + var properties = EntityHelper + .GetEntityTypeMetadata(valuesType) + .MappedProperties.Where(a => a.CanRead) + .ToList(); + + for (var i = 0; i < properties.Count; i++) + { + if (i > 0) + { + sqlBuilder.Append(", "); + } + + var property = properties[i]; + + sqlBuilder.Append('"'); + sqlBuilder.Append(property.ColumnName); + sqlBuilder.Append('"'); + + parameters[i] = new() { ParameterName = property.PropertyName }; + } + } + + sqlBuilder.AppendLine(")"); + + sqlBuilder.AppendLine("VALUES"); + + sqlBuilder.Append(Constants.Indent); + sqlBuilder.Append("("); + + for (var i = 0; i < fieldCount; i++) + { + if (i > 0) + { + sqlBuilder.Append(", "); + } + + sqlBuilder.Append(":\"" + parameters[i].ParameterName + "\""); + } + + sqlBuilder.AppendLine(")"); + + return (sqlBuilder.ToString(), parameters); + } + + /// + /// 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 quoted 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 quotedTableName, + OracleConnection connection, + OracleTransaction? transaction + ) + { + using var command = connection.CreateCommand(); + + command.CommandText = $"DROP TABLE {quotedTableName}"; + command.Transaction = transaction; + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); + + command.ExecuteNonQuery(); + } + + /// + /// Asynchronously drops the temporary table with the specified name. + /// + /// The quoted 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 quotedTableName, + OracleConnection connection, + OracleTransaction? transaction + ) + { +#pragma warning disable CA2007 + await using var command = connection.CreateCommand(); +#pragma warning restore CA2007 + + command.CommandText = $"DROP TABLE {quotedTableName}"; + command.Transaction = transaction; + + DbConnectionExtensions.OnBeforeExecutingCommand(command, []); + + await command.ExecuteNonQueryAsync().ConfigureAwait(false); + } + /// /// Builds an SQL code to create a multi-column temporary table to be populated with objects of the type /// . @@ -234,14 +387,13 @@ await this.PopulateTemporaryTableAsync( /// 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 quotedTableName, - [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] - Type objectsType, + private string BuildCreateMultiColumnTemporaryTableSqlCode( + string quotedTableName, + [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type objectsType, EnumSerializationMode enumSerializationMode ) { - using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]); + using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]); sqlBuilder.Append("CREATE PRIVATE TEMPORARY TABLE "); sqlBuilder.AppendLine(quotedTableName); @@ -285,15 +437,14 @@ EnumSerializationMode enumSerializationMode /// 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 quotedTableName, + private string BuildCreateSingleColumnTemporaryTableSqlCode( + string quotedTableName, IEnumerable values, - [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] - Type valuesType, + [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType, EnumSerializationMode enumSerializationMode ) { - using var sqlBuilder = new ValueStringBuilder(stackalloc Char[100]); + using var sqlBuilder = new ValueStringBuilder(stackalloc char[100]); sqlBuilder.Append("CREATE PRIVATE TEMPORARY TABLE"); sqlBuilder.AppendLine(quotedTableName); @@ -303,11 +454,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) { @@ -354,9 +505,8 @@ EnumSerializationMode enumSerializationMode private void PopulateTemporaryTable( OracleConnection connection, OracleTransaction? transaction, - String quotedTableName, - [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] - Type valuesType, + string quotedTableName, + [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType, DbDataReader dataReader, CancellationToken cancellationToken ) @@ -403,9 +553,8 @@ CancellationToken cancellationToken private async Task PopulateTemporaryTableAsync( OracleConnection connection, OracleTransaction? transaction, - String quotedTableName, - [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] - Type valuesType, + string quotedTableName, + [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType, DbDataReader dataReader, CancellationToken cancellationToken ) @@ -439,160 +588,4 @@ CancellationToken cancellationToken await insertCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } } - - /// - /// Builds an SQL code to insert data from the specified data reader into the specified temporary table. - /// - /// The quoted name of the table to insert data into. - /// 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, OracleParameter[] Parameters) BuildInsertSqlCode( - String quotedTableName, - [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] - Type valuesType, - DbDataReader dataReader - ) - { - using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]); - - sqlBuilder.Append("INSERT INTO "); - sqlBuilder.AppendLine(quotedTableName); - - sqlBuilder.Append(Constants.Indent); - sqlBuilder.Append("("); - - var fieldCount = dataReader.FieldCount; - var parameters = new OracleParameter[fieldCount]; - - if (valuesType.IsBuiltInTypeOrNullableBuiltInType() || valuesType.IsEnumOrNullableEnumType()) - { - sqlBuilder.Append("\""); - sqlBuilder.Append(Constants.SingleColumnTemporaryTableColumnName); - sqlBuilder.Append("\""); - - parameters[0] = new() - { - ParameterName = Constants.SingleColumnTemporaryTableColumnName - }; - } - else - { - var properties = EntityHelper.GetEntityTypeMetadata(valuesType).MappedProperties.Where(a => a.CanRead) - .ToList(); - - for (var i = 0; i < properties.Count; i++) - { - if (i > 0) - { - sqlBuilder.Append(", "); - } - - var property = properties[i]; - - sqlBuilder.Append('"'); - sqlBuilder.Append(property.ColumnName); - sqlBuilder.Append('"'); - - parameters[i] = new() - { - ParameterName = property.PropertyName - }; - } - } - - sqlBuilder.AppendLine(")"); - - sqlBuilder.AppendLine("VALUES"); - - sqlBuilder.Append(Constants.Indent); - sqlBuilder.Append("("); - - for (var i = 0; i < fieldCount; i++) - { - if (i > 0) - { - sqlBuilder.Append(", "); - } - - sqlBuilder.Append(":\"" + parameters[i].ParameterName + "\""); - } - - sqlBuilder.AppendLine(")"); - - return (sqlBuilder.ToString(), parameters); - } - - /// - /// 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 quoted 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 quotedTableName, - OracleConnection connection, - OracleTransaction? transaction - ) - { - using var command = connection.CreateCommand(); - - command.CommandText = $"DROP TABLE {quotedTableName}"; - command.Transaction = transaction; - - DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - - command.ExecuteNonQuery(); - } - - /// - /// Asynchronously drops the temporary table with the specified name. - /// - /// The quoted 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 quotedTableName, - OracleConnection connection, - OracleTransaction? transaction - ) - { -#pragma warning disable CA2007 - await using var command = connection.CreateCommand(); -#pragma warning restore CA2007 - - command.CommandText = $"DROP TABLE {quotedTableName}"; - command.Transaction = transaction; - - DbConnectionExtensions.OnBeforeExecutingCommand(command, []); - - await command.ExecuteNonQueryAsync().ConfigureAwait(false); - } - - private readonly OracleDatabaseAdapter databaseAdapter; } diff --git a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/GlobalUsings.cs b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/GlobalUsings.cs index cfc88dd..61f5ddc 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/GlobalUsings.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/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.PostgreSql/PostgreSqlConfigurationExtensions.cs b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlConfigurationExtensions.cs index a2d3507..bd2f49d 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlConfigurationExtensions.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlConfigurationExtensions.cs @@ -2,7 +2,9 @@ using RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; #pragma warning disable IDE0130 +// ReSharper disable once CheckNamespace namespace RentADeveloper.DbConnectionPlus.Configuration; + #pragma warning restore IDE0130 /// diff --git a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlDatabaseAdapter.cs b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlDatabaseAdapter.cs index db86d96..19c6971 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlDatabaseAdapter.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlDatabaseAdapter.cs @@ -11,6 +11,49 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; /// public class PostgreSqlDatabaseAdapter : IDatabaseAdapter { + private static readonly Dictionary typeToNpgsqlDbType = new() + { + { typeof(bool), NpgsqlDbType.Boolean }, + { typeof(byte), NpgsqlDbType.Smallint }, + { typeof(byte[]), NpgsqlDbType.Bytea }, + { typeof(char), NpgsqlDbType.Char }, + { typeof(DateOnly), NpgsqlDbType.Date }, + { typeof(DateTime), NpgsqlDbType.Timestamp }, + { typeof(decimal), NpgsqlDbType.Numeric }, + { typeof(double), NpgsqlDbType.Double }, + { typeof(Guid), NpgsqlDbType.Uuid }, + { typeof(short), NpgsqlDbType.Smallint }, + { typeof(int), NpgsqlDbType.Integer }, + { typeof(long), NpgsqlDbType.Bigint }, + { typeof(float), NpgsqlDbType.Real }, + { typeof(string), NpgsqlDbType.Text }, + { typeof(TimeOnly), NpgsqlDbType.Time }, + { typeof(TimeSpan), NpgsqlDbType.Interval }, + }; + + private static readonly Dictionary typeToPostgreSqlDataType = new() + { + { typeof(bool), "boolean" }, + { typeof(byte), "smallint" }, + { typeof(byte[]), "bytea" }, + { typeof(char), "char(1)" }, + { typeof(DateOnly), "date" }, + { typeof(DateTime), "timestamp without time zone" }, + { typeof(decimal), "decimal" }, + { typeof(double), "double precision" }, + { typeof(Guid), "uuid" }, + { typeof(short), "smallint" }, + { typeof(int), "integer" }, + { typeof(long), "bigint" }, + { typeof(float), "real" }, + { typeof(string), "text" }, + { typeof(TimeOnly), "time" }, + { typeof(TimeSpan), "interval" }, + }; + + private readonly PostgreSqlEntityManipulator entityManipulator; + private readonly PostgreSqlTemporaryTableBuilder temporaryTableBuilder; + /// /// Initializes a new instance of the class. /// @@ -24,11 +67,10 @@ public PostgreSqlDatabaseAdapter() 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); @@ -42,16 +84,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( @@ -60,7 +99,7 @@ public void BindParameterValue(DbParameter parameter, Object? value) ); break; - case Byte[]: + case byte[]: parameter.DbType = DbType.Binary; parameter.Value = value; break; @@ -72,11 +111,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); @@ -87,14 +125,11 @@ public String GetDataType(Type type, EnumSerializationMode enumSerializationMode { return enumSerializationMode switch { - EnumSerializationMode.Strings => - "character varying(200)", // 200 should be enough for most enum names + EnumSerializationMode.Strings => "character varying(200)", // 200 should be enough for most enum names - EnumSerializationMode.Integers => - "integer", + EnumSerializationMode.Integers => "integer", - _ => - ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode) + _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode), }; } @@ -150,14 +185,11 @@ public NpgsqlDbType GetDbType(Type type, EnumSerializationMode enumSerialization { return enumSerializationMode switch { - EnumSerializationMode.Strings => - NpgsqlDbType.Varchar, + EnumSerializationMode.Strings => NpgsqlDbType.Varchar, - EnumSerializationMode.Integers => - NpgsqlDbType.Integer, + EnumSerializationMode.Integers => NpgsqlDbType.Integer, - _ => - ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode) + _ => ThrowHelper.ThrowInvalidEnumSerializationModeException(enumSerializationMode), }; } @@ -174,68 +206,19 @@ public NpgsqlDbType GetDbType(Type type, EnumSerializationMode enumSerialization } /// - 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); return cancellationToken.IsCancellationRequested && exception is OperationCanceledException; } - - private readonly PostgreSqlEntityManipulator entityManipulator; - private readonly PostgreSqlTemporaryTableBuilder temporaryTableBuilder; - - private static readonly Dictionary typeToNpgsqlDbType = new() - { - { typeof(Boolean), NpgsqlDbType.Boolean }, - { typeof(Byte), NpgsqlDbType.Smallint }, - { typeof(Byte[]), NpgsqlDbType.Bytea }, - { typeof(Char), NpgsqlDbType.Char }, - { typeof(DateOnly), NpgsqlDbType.Date }, - { typeof(DateTime), NpgsqlDbType.Timestamp }, - { typeof(Decimal), NpgsqlDbType.Numeric }, - { typeof(Double), NpgsqlDbType.Double }, - { typeof(Guid), NpgsqlDbType.Uuid }, - { typeof(Int16), NpgsqlDbType.Smallint }, - { typeof(Int32), NpgsqlDbType.Integer }, - { typeof(Int64), NpgsqlDbType.Bigint }, - { typeof(Single), NpgsqlDbType.Real }, - { typeof(String), NpgsqlDbType.Text }, - { typeof(TimeOnly), NpgsqlDbType.Time }, - { typeof(TimeSpan), NpgsqlDbType.Interval } - }; - - private static readonly Dictionary typeToPostgreSqlDataType = new() - { - { typeof(Boolean), "boolean" }, - { typeof(Byte), "smallint" }, - { typeof(Byte[]), "bytea" }, - { typeof(Char), "char(1)" }, - { typeof(DateOnly), "date" }, - { typeof(DateTime), "timestamp without time zone" }, - { typeof(Decimal), "decimal" }, - { typeof(Double), "double precision" }, - { typeof(Guid), "uuid" }, - { typeof(Int16), "smallint" }, - { typeof(Int32), "integer" }, - { typeof(Int64), "bigint" }, - { typeof(Single), "real" }, - { typeof(String), "text" }, - { typeof(TimeOnly), "time" }, - { typeof(TimeSpan), "interval" } - }; } diff --git a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlEntityManipulator.cs b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlEntityManipulator.cs index 5cf7d77..51752c6 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlEntityManipulator.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlEntityManipulator.cs @@ -4,26 +4,22 @@ using LinkDotNet.StringBuilder; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; -using RentADeveloper.DbConnectionPlus.Entities; namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; /// /// The entity manipulator for PostgreSQL. /// -internal class PostgreSqlEntityManipulator : IEntityManipulator +/// The database adapter to use to manipulate entities. +internal class PostgreSqlEntityManipulator(PostgreSqlDatabaseAdapter databaseAdapter) : IEntityManipulator { - /// - /// Initializes a new instance of the class. - /// - /// The database adapter to use to manipulate entities. - public PostgreSqlEntityManipulator(PostgreSqlDatabaseAdapter databaseAdapter) => - this.databaseAdapter = databaseAdapter; + private readonly PostgreSqlDatabaseAdapter 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 RETURNING 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 keyProperty 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"); @@ -1049,8 +1042,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 PostgreSqlDatabaseAdapter databaseAdapter; - private readonly ConcurrentDictionary entityDeleteSqlCodePerEntityType = new(); - private readonly ConcurrentDictionary entityInsertSqlCodePerEntityType = new(); - private readonly ConcurrentDictionary entityUpdateSqlCodePerEntityType = new(); } diff --git a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlTemporaryTableBuilder.cs b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlTemporaryTableBuilder.cs index d97224c..4f46cd4 100644 --- a/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlTemporaryTableBuilder.cs +++ b/src/DbConnectionPlus.DatabaseAdapters.PostgreSql/PostgreSqlTemporaryTableBuilder.cs @@ -6,7 +6,6 @@ using NpgsqlTypes; using RentADeveloper.DbConnectionPlus.Converters; using RentADeveloper.DbConnectionPlus.DbCommands; -using RentADeveloper.DbConnectionPlus.Entities; using RentADeveloper.DbConnectionPlus.Extensions; using RentADeveloper.DbConnectionPlus.Readers; @@ -17,6 +16,8 @@ namespace RentADeveloper.DbConnectionPlus.DatabaseAdapters.PostgreSql; /// internal class PostgreSqlTemporaryTableBuilder : ITemporaryTableBuilder { + private readonly PostgreSqlDatabaseAdapter databaseAdapter; + /// /// Initializes a new instance of the class. /// @@ -35,10 +36,9 @@ public PostgreSqlTemporaryTableBuilder(PostgreSqlDatabaseAdapter 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 ) { @@ -70,8 +70,10 @@ public TemporaryTableDisposer BuildTemporaryTable( ); createCommand.Transaction = transaction; - using var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); + using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + createCommand, + cancellationToken + ); DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []); @@ -88,8 +90,10 @@ public TemporaryTableDisposer BuildTemporaryTable( ); createCommand.Transaction = transaction; - using var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(createCommand, cancellationToken); + using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + createCommand, + cancellationToken + ); DbConnectionExtensions.OnBeforeExecutingCommand(createCommand, []); @@ -110,10 +114,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 ) { @@ -147,8 +150,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, []); @@ -167,8 +171,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, []); @@ -188,6 +193,74 @@ await this.PopulateTemporaryTableAsync(npgsqlConnection, name, valuesType, reade ); } + /// + /// 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, 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); + } + /// /// Builds an SQL code to create a multi-column temporary table to be populated with objects of the type /// . @@ -196,14 +269,13 @@ await this.PopulateTemporaryTableAsync(npgsqlConnection, name, valuesType, reade /// 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, + private string BuildCreateMultiColumnTemporaryTableSqlCode( + string tableName, + [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type objectsType, EnumSerializationMode enumSerializationMode ) { - using var sqlBuilder = new ValueStringBuilder(stackalloc Char[500]); + using var sqlBuilder = new ValueStringBuilder(stackalloc char[500]); sqlBuilder.Append("CREATE TEMP TABLE \""); sqlBuilder.Append(tableName); @@ -247,14 +319,13 @@ EnumSerializationMode enumSerializationMode /// 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, + private string BuildCreateSingleColumnTemporaryTableSqlCode( + string tableName, + [DynamicallyAccessedMembers(EntityHelper.TemporaryTableValueMemberTypes)] Type valuesType, EnumSerializationMode enumSerializationMode ) { - using var sqlBuilder = new ValueStringBuilder(stackalloc Char[100]); + using var sqlBuilder = new ValueStringBuilder(stackalloc char[100]); sqlBuilder.Append("CREATE TEMP TABLE \""); sqlBuilder.Append(tableName); @@ -285,8 +356,8 @@ EnumSerializationMode enumSerializationMode /// , so reading the property types here is warning-free and yields the same values. /// 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-all True - 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( } catch (Exception ex) { - return Task.FromException(ex); + return Task.FromException(ex); } } - - private readonly IEntityManipulator manipulator; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs index 87bde38..22b127e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.InsertEntityTests.cs @@ -3,38 +3,34 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; -public sealed class - EntityManipulator_InsertEntityTests_MySql : - EntityManipulator_InsertEntityTests; +public sealed class EntityManipulator_InsertEntityTests_MySql + : EntityManipulator_InsertEntityTests; -public sealed class - EntityManipulator_InsertEntityTests_Oracle : - EntityManipulator_InsertEntityTests; +public sealed class EntityManipulator_InsertEntityTests_Oracle + : EntityManipulator_InsertEntityTests; -public sealed class - EntityManipulator_InsertEntityTests_PostgreSql : - EntityManipulator_InsertEntityTests; +public sealed class EntityManipulator_InsertEntityTests_PostgreSql + : EntityManipulator_InsertEntityTests; -public sealed class - EntityManipulator_InsertEntityTests_Sqlite : - EntityManipulator_InsertEntityTests; +public sealed class EntityManipulator_InsertEntityTests_Sqlite + : EntityManipulator_InsertEntityTests; -public sealed class - EntityManipulator_InsertEntityTests_SqlServer : - EntityManipulator_InsertEntityTests; +public sealed class EntityManipulator_InsertEntityTests_SqlServer + : EntityManipulator_InsertEntityTests; -public abstract class EntityManipulator_InsertEntityTests - : IntegrationTestsBase +public abstract class EntityManipulator_InsertEntityTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { + private readonly IEntityManipulator manipulator; + /// - protected EntityManipulator_InsertEntityTests() => - this.manipulator = this.DatabaseAdapter.EntityManipulator; + protected EntityManipulator_InsertEntityTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -44,21 +40,19 @@ public async Task InsertEntity_CancellationToken_ShouldCancelOperationIfCancella this.DelayNextDbCommand = true; - await Invoking(() => - this.CallApi(useAsyncApi, this.Connection, entity, null, cancellationToken) - ) - .Should().ThrowAsync() + await Invoking(() => this.CallApi(useAsyncApi, this.Connection, entity, null, cancellationToken)) + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); // Since the operation was cancelled, the entity should not have been inserted. - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entity).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(Boolean useAsyncApi) + public async Task InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(bool useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -66,17 +60,20 @@ public async Task InsertEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumVa await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be((Int32)entity.Enum); + ) + ) + .Should() + .Be((int)entity.Enum); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) + public async Task InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(bool useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -84,43 +81,43 @@ public async Task InsertEntity_EnumSerializationModeIsStrings_ShouldStoreEnumVal await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity.Enum.ToString()); + ) + ) + .Should() + .BeEquivalentTo(entity.Enum.ToString()); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task InsertEntity_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entity = Generate.Single(); entity.Computed_ = 0; entity.Identity_ = 0; entity.NotMapped = "ShouldNotBePersisted"; - await this.CallApi( - useAsyncApi, - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); (await this.Connection.QueryFirstAsync($"SELECT * FROM {Q("MappingTestEntity")}")) - .Should().BeEquivalentTo( + .Should() + .BeEquivalentTo( entity, - 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 InsertEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task InsertEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); @@ -129,73 +126,70 @@ public async Task InsertEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boole entity.Identity_ = 0; entity.NotMapped = "ShouldNotBePersisted"; - await this.CallApi( - useAsyncApi, - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); (await this.Connection.QueryFirstAsync($"SELECT * FROM {Q("MappingTestEntity")}")) - .Should().BeEquivalentTo( + .Should() + .BeEquivalentTo( entity, - 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 InsertEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task InsertEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi) { var entity = Generate.Single(); - await this.CallApi( - useAsyncApi, - this.Connection, - entity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); (await this.Connection.QueryFirstAsync($"SELECT * FROM {Q("MappingTestEntity")}")) - .Should().BeEquivalentTo(entity); + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_ShouldInsertEntity(Boolean useAsyncApi) + public async Task InsertEntity_ShouldInsertEntity(bool useAsyncApi) { var entity = Generate.Single(); (await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken)) - .Should().Be(1); + .Should() + .Be(1); - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task InsertEntity_ShouldReturnNumberOfAffectedRows(bool useAsyncApi) { var entity = Generate.Single(); (await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken)) - .Should().Be(1); + .Should() + .Be(1); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task InsertEntity_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); @@ -203,43 +197,47 @@ public async Task InsertEntity_ShouldSupportDateTimeOffsetValues(Boolean useAsyn await this.CallApi(useAsyncApi, this.Connection, entity, null, TestContext.Current.CancellationToken); - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task InsertEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task InsertEntity_Transaction_ShouldUseTransaction(bool useAsyncApi) { var entity = Generate.Single(); await using (var transaction = await this.Connection.BeginTransactionAsync()) { - (await this.CallApi( + ( + await this.CallApi( useAsyncApi, this.Connection, entity, transaction, TestContext.Current.CancellationToken - )) - .Should().Be(1); + ) + ) + .Should() + .Be(1); - this.ExistsEntityInDb(entity, transaction) - .Should().BeTrue(); + this.ExistsEntityInDb(entity, transaction).Should().BeTrue(); await transaction.RollbackAsync(); } - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entity).Should().BeFalse(); } - private Task CallApi( - Boolean useAsyncApi, + private Task CallApi( + bool useAsyncApi, DbConnection connection, TEntity entity, DbTransaction? transaction = null, @@ -254,15 +252,11 @@ private Task CallApi( try { - return Task.FromResult( - this.manipulator.InsertEntity(connection, entity, transaction, cancellationToken) - ); + return Task.FromResult(this.manipulator.InsertEntity(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.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs index 9a14194..ab314e0 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntitiesTests.cs @@ -4,40 +4,34 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; -public sealed class - EntityManipulator_UpdateEntitiesTests_MySql : - EntityManipulator_UpdateEntitiesTests; +public sealed class EntityManipulator_UpdateEntitiesTests_MySql + : EntityManipulator_UpdateEntitiesTests; -public sealed class - EntityManipulator_UpdateEntitiesTests_Oracle : - EntityManipulator_UpdateEntitiesTests; +public sealed class EntityManipulator_UpdateEntitiesTests_Oracle + : EntityManipulator_UpdateEntitiesTests; -public sealed class - EntityManipulator_UpdateEntitiesTests_PostgreSql : - EntityManipulator_UpdateEntitiesTests; +public sealed class EntityManipulator_UpdateEntitiesTests_PostgreSql + : EntityManipulator_UpdateEntitiesTests; -public sealed class - EntityManipulator_UpdateEntitiesTests_Sqlite : - EntityManipulator_UpdateEntitiesTests; +public sealed class EntityManipulator_UpdateEntitiesTests_Sqlite + : EntityManipulator_UpdateEntitiesTests; -public sealed class - EntityManipulator_UpdateEntitiesTests_SqlServer : - EntityManipulator_UpdateEntitiesTests; +public sealed class EntityManipulator_UpdateEntitiesTests_SqlServer + : EntityManipulator_UpdateEntitiesTests; -public abstract class EntityManipulator_UpdateEntitiesTests - : IntegrationTestsBase +public abstract class EntityManipulator_UpdateEntitiesTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { + private readonly IEntityManipulator manipulator; + /// - protected EntityManipulator_UpdateEntitiesTests() => - this.manipulator = this.DatabaseAdapter.EntityManipulator; + protected EntityManipulator_UpdateEntitiesTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task UpdateEntities_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -48,84 +42,92 @@ Boolean useAsyncApi this.DelayNextDbCommand = true; - await Invoking(() => - this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, cancellationToken) - ) - .Should().ThrowAsync() + await Invoking(() => this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, cancellationToken)) + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); // Since the operation was cancelled, the entities should not have been updated. - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + 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 UpdateEntities_ConcurrencyTokenMismatch_ShouldThrow(Boolean useAsyncApi) + public async Task UpdateEntities_ConcurrencyTokenMismatch_ShouldThrow(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdateFor(entities); var failingEntity = updatedEntities[^1]; - failingEntity.ConcurrencyToken_ = Generate.Single(); - - var exception = (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken + failingEntity.ConcurrencyToken_ = Generate.Single(); + + var exception = ( + await Invoking(() => + this.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + 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 updatedEntities.Except([failingEntity])) { - (await this.Connection.QueryFirstAsync( + ( + await this.Connection.QueryFirstAsync( $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND - {Q("Key2")} = {Parameter(entity.Key2_)} - """, + SELECT * + FROM {Q("MappingTestEntity")} + WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND + {Q("Key2")} = {Parameter(entity.Key2_)} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ).Should().BeEquivalentTo(entity); } - (await this.Connection.QueryFirstAsync( + ( + await this.Connection.QueryFirstAsync( $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND - {Q("Key2")} = {Parameter(failingEntity.Key2_)} - """, + SELECT * + FROM {Q("MappingTestEntity")} + WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND + {Q("Key2")} = {Parameter(failingEntity.Key2_)} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[^1]); + ) + ).Should().BeEquivalentTo(entities[^1]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( - Boolean useAsyncApi - ) + public async Task UpdateEntities_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(bool useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -139,34 +141,38 @@ await this.manipulator.InsertEntitiesAsync( ); // Make sure the enums are stored as integers: - (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 + .Connection.QueryAsync( + $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities.Select(a => (int)a.Enum)); var updatedEntities = Generate.UpdateFor(entities); - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); // Make sure the enums are stored as integers: - (await this.Connection.QueryAsync( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities.Select(a => (Int32)a.Enum)); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(updatedEntities.Select(a => (int)a.Enum)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) + public async Task UpdateEntities_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(bool useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -180,59 +186,58 @@ await this.manipulator.InsertEntitiesAsync( ); // Make sure the enums are stored as strings: - (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 + .Connection.QueryAsync( + $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities.Select(a => a.Enum.ToString())); var updatedEntities = Generate.UpdateFor(entities); - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); // Make sure the enums are stored as strings: - (await this.Connection.QueryAsync( - $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities.Select(a => a.Enum.ToString())); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(updatedEntities.Select(a => a.Enum.ToString())); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task UpdateEntities_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdateFor(entities); updatedEntities.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, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") - .Should().BeEquivalentTo( + .Should() + .BeEquivalentTo( updatedEntities, options => - options.Using(context => context.Subject.Should().BeNull()) + options + .Using(context => context.Subject.Should().BeNull()) .When(info => info.Path.EndsWith("NotMapped")) ); } @@ -240,7 +245,7 @@ await this.CallApi( [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task UpdateEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); @@ -248,37 +253,34 @@ public async Task UpdateEntities_Mapping_FluentApi_ShouldUseFluentApiMapping(Boo var updatedEntities = Generate.UpdateFor(entities); updatedEntities.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, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") - .Should().BeEquivalentTo( + .Should() + .BeEquivalentTo( updatedEntities, - 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 Task UpdateEntities_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + public Task UpdateEntities_Mapping_MissingKeyProperty_ShouldThrow(bool useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - return Invoking(() => this.CallApi( + return Invoking(() => + this.CallApi( useAsyncApi, this.Connection, [entityWithoutKeyProperty], @@ -286,171 +288,170 @@ public Task UpdateEntities_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 UpdateEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task UpdateEntities_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdateFor(entities); - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); this.Connection.Query($"SELECT * FROM {Q("MappingTestEntity")}") - .Should().BeEquivalentTo(updatedEntities); + .Should() + .BeEquivalentTo(updatedEntities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_RowVersionMismatch_ShouldThrow(Boolean useAsyncApi) + public async Task UpdateEntities_RowVersionMismatch_ShouldThrow(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdateFor(entities); var failingEntity = updatedEntities[^1]; - failingEntity.RowVersion_ = Generate.Single(); - - var exception = (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken + failingEntity.RowVersion_ = Generate.Single(); + + var exception = ( + await Invoking(() => + this.CallApi( + useAsyncApi, + this.Connection, + updatedEntities, + 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 updatedEntities.Except([failingEntity])) { - (await this.Connection.QueryFirstAsync( + ( + await this.Connection.QueryFirstAsync( $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND - {Q("Key2")} = {Parameter(entity.Key2_)} - """, + SELECT * + FROM {Q("MappingTestEntity")} + WHERE {Q("Key1")} = {Parameter(entity.Key1_)} AND + {Q("Key2")} = {Parameter(entity.Key2_)} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ).Should().BeEquivalentTo(entity); } - (await this.Connection.QueryFirstAsync( + ( + await this.Connection.QueryFirstAsync( $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND - {Q("Key2")} = {Parameter(failingEntity.Key2_)} - """, + SELECT * + FROM {Q("MappingTestEntity")} + WHERE {Q("Key1")} = {Parameter(failingEntity.Key1_)} AND + {Q("Key2")} = {Parameter(failingEntity.Key2_)} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[^1]); + ) + ).Should().BeEquivalentTo(entities[^1]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldReturnNumberOfAffectedRows(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdateFor(entities); - (await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - )) - .Should().Be(entities.Count); + (await this.CallApi(useAsyncApi, this.Connection, updatedEntities, 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 UpdateEntities_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdateFor(entities); - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken); - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities); + ( + await this + .Connection.QueryAsync( + $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(updatedEntities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_ShouldUpdateEntities(Boolean useAsyncApi) + public async Task UpdateEntities_ShouldUpdateEntities(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); var updatedEntities = Generate.UpdateFor(entities); - (await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntities, - null, - TestContext.Current.CancellationToken - )) - .Should().Be(updatedEntities.Count); + (await this.CallApi(useAsyncApi, this.Connection, updatedEntities, null, TestContext.Current.CancellationToken)) + .Should() + .Be(updatedEntities.Count); - (await this.Connection.QueryAsync( - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities); + ( + await this + .Connection.QueryAsync( + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(updatedEntities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntities_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task UpdateEntities_Transaction_ShouldUseTransaction(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); @@ -458,29 +459,40 @@ public async Task UpdateEntities_Transaction_ShouldUseTransaction(Boolean useAsy { var updatedEntities = Generate.UpdateFor(entities); - (await this.CallApi( + ( + await this.CallApi( useAsyncApi, this.Connection, updatedEntities, transaction, TestContext.Current.CancellationToken - )) - .Should().Be(entities.Count); + ) + ) + .Should() + .Be(entities.Count); - (await this.Connection.QueryAsync($"SELECT * FROM {Q("Entity")}", transaction) - .ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(updatedEntities); + ( + await this + .Connection.QueryAsync($"SELECT * FROM {Q("Entity")}", transaction) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(updatedEntities); await transaction.RollbackAsync(); } - (await this.Connection.QueryAsync($"SELECT * FROM {Q("Entity")}") - .ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await this + .Connection.QueryAsync($"SELECT * FROM {Q("Entity")}") + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } - private Task CallApi( - Boolean useAsyncApi, + private Task CallApi( + bool useAsyncApi, DbConnection connection, IEnumerable entities, DbTransaction? transaction = null, @@ -501,9 +513,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.UpdateEntityTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs index 609177c..67e5d25 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/EntityManipulator.UpdateEntityTests.cs @@ -4,38 +4,34 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; -public sealed class - EntityManipulator_UpdateEntityTests_MySql : - EntityManipulator_UpdateEntityTests; +public sealed class EntityManipulator_UpdateEntityTests_MySql + : EntityManipulator_UpdateEntityTests; -public sealed class - EntityManipulator_UpdateEntityTests_Oracle : - EntityManipulator_UpdateEntityTests; +public sealed class EntityManipulator_UpdateEntityTests_Oracle + : EntityManipulator_UpdateEntityTests; -public sealed class - EntityManipulator_UpdateEntityTests_PostgreSql : - EntityManipulator_UpdateEntityTests; +public sealed class EntityManipulator_UpdateEntityTests_PostgreSql + : EntityManipulator_UpdateEntityTests; -public sealed class - EntityManipulator_UpdateEntityTests_Sqlite : - EntityManipulator_UpdateEntityTests; +public sealed class EntityManipulator_UpdateEntityTests_Sqlite + : EntityManipulator_UpdateEntityTests; -public sealed class - EntityManipulator_UpdateEntityTests_SqlServer : - EntityManipulator_UpdateEntityTests; +public sealed class EntityManipulator_UpdateEntityTests_SqlServer + : EntityManipulator_UpdateEntityTests; -public abstract class EntityManipulator_UpdateEntityTests - : IntegrationTestsBase +public abstract class EntityManipulator_UpdateEntityTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { + private readonly IEntityManipulator manipulator; + /// - protected EntityManipulator_UpdateEntityTests() => - this.manipulator = this.DatabaseAdapter.EntityManipulator; + protected EntityManipulator_UpdateEntityTests() => this.manipulator = this.DatabaseAdapter.EntityManipulator; [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -46,67 +42,74 @@ public async Task UpdateEntity_CancellationToken_ShouldCancelOperationIfCancella this.DelayNextDbCommand = true; - await Invoking(() => - this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, cancellationToken) - ) - .Should().ThrowAsync() + await Invoking(() => this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, cancellationToken)) + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); // Since the operation was cancelled, the entity should not have been updated. - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_ConcurrencyTokenMismatch_ShouldThrow(Boolean useAsyncApi) + public async Task UpdateEntity_ConcurrencyTokenMismatch_ShouldThrow(bool useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.ConcurrencyToken_ = Generate.Single(); - - var exception = (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken + updatedEntity.ConcurrencyToken_ = Generate.Single(); + + var exception = ( + await Invoking(() => + this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + 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(updatedEntity); + exception.Entity.Should().Be(updatedEntity); - (await this.Connection.QueryFirstAsync( + ( + await this.Connection.QueryFirstAsync( $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND - {Q("Key2")} = {Parameter(updatedEntity.Key2_)} - """, + SELECT * + FROM {Q("MappingTestEntity")} + WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND + {Q("Key2")} = {Parameter(updatedEntity.Key2_)} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ).Should().BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(Boolean useAsyncApi) + public async Task UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers(bool useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -115,34 +118,34 @@ public async Task UpdateEntity_EnumSerializationModeIsIntegers_ShouldStoreEnumVa await this.manipulator.InsertEntityAsync(this.Connection, entity, null, TestContext.Current.CancellationToken); // Make sure the enum is stored as integer: - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be((Int32)entity.Enum); + ) + ) + .Should() + .Be((int)entity.Enum); var updatedEntity = Generate.UpdateFor(entity); - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); // Make sure the enum is stored as integer: - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsInteger")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be((Int32)updatedEntity.Enum); + ) + ) + .Should() + .Be((int)updatedEntity.Enum); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(Boolean useAsyncApi) + public async Task UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings(bool useAsyncApi) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -151,34 +154,34 @@ public async Task UpdateEntity_EnumSerializationModeIsStrings_ShouldStoreEnumVal await this.manipulator.InsertEntityAsync(this.Connection, entity, null, TestContext.Current.CancellationToken); // Make sure the enum is stored as string: - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity.Enum.ToString()); + ) + ) + .Should() + .BeEquivalentTo(entity.Enum.ToString()); var updatedEntity = Generate.UpdateFor(entity); - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); // Make sure the enum is stored as string: - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT {Q("Enum")} FROM {Q("EntityWithEnumStoredAsString")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity.Enum.ToString()); + ) + ) + .Should() + .BeEquivalentTo(updatedEntity.Enum.ToString()); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task UpdateEntity_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -187,26 +190,23 @@ public async Task UpdateEntity_Mapping_Attributes_ShouldUseAttributesMapping(Boo updatedEntity.Identity_ = 0; updatedEntity.NotMapped = "ShouldNotBePersisted"; - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); (await this.Connection.QueryFirstAsync($"SELECT * FROM {Q("MappingTestEntity")}")) - .Should().BeEquivalentTo( + .Should() + .BeEquivalentTo( updatedEntity, - 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 UpdateEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task UpdateEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); @@ -217,30 +217,28 @@ public async Task UpdateEntity_Mapping_FluentApi_ShouldUseFluentApiMapping(Boole updatedEntity.Identity_ = 0; updatedEntity.NotMapped = "ShouldNotBePersisted"; - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); (await this.Connection.QueryFirstAsync($"SELECT * FROM {Q("MappingTestEntity")}")) - .Should().BeEquivalentTo( + .Should() + .BeEquivalentTo( updatedEntity, - 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 Task UpdateEntity_Mapping_MissingKeyProperty_ShouldThrow(Boolean useAsyncApi) + public Task UpdateEntity_Mapping_MissingKeyProperty_ShouldThrow(bool useAsyncApi) { var entityWithoutKeyProperty = new EntityWithoutKeyProperty(); - return Invoking(() => this.CallApi( + return Invoking(() => + this.CallApi( useAsyncApi, this.Connection, entityWithoutKeyProperty, @@ -248,147 +246,138 @@ public Task UpdateEntity_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 UpdateEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task UpdateEntity_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); (await this.Connection.QueryFirstAsync($"SELECT * FROM {Q("MappingTestEntity")}")) - .Should().BeEquivalentTo(updatedEntity); + .Should() + .BeEquivalentTo(updatedEntity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_RowVersionMismatch_ShouldThrow(Boolean useAsyncApi) + public async Task UpdateEntity_RowVersionMismatch_ShouldThrow(bool useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - updatedEntity.RowVersion_ = Generate.Single(); - - var exception = (await Invoking(() => this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken + updatedEntity.RowVersion_ = Generate.Single(); + + var exception = ( + await Invoking(() => + this.CallApi( + useAsyncApi, + this.Connection, + updatedEntity, + 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(updatedEntity); + exception.Entity.Should().Be(updatedEntity); - (await this.Connection.QueryFirstAsync( + ( + await this.Connection.QueryFirstAsync( $""" - SELECT * - FROM {Q("MappingTestEntity")} - WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND - {Q("Key2")} = {Parameter(updatedEntity.Key2_)} - """, + SELECT * + FROM {Q("MappingTestEntity")} + WHERE {Q("Key1")} = {Parameter(updatedEntity.Key1_)} AND + {Q("Key2")} = {Parameter(updatedEntity.Key2_)} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ).Should().BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldReturnNumberOfAffectedRows(bool useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - (await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - )) - .Should().Be(1); + (await this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken)) + .Should() + .Be(1); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - ); + await this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken); - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity); + ) + ) + .Should() + .BeEquivalentTo(updatedEntity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_ShouldUpdateEntity(Boolean useAsyncApi) + public async Task UpdateEntity_ShouldUpdateEntity(bool useAsyncApi) { var entity = this.CreateEntityInDb(); var updatedEntity = Generate.UpdateFor(entity); - (await this.CallApi( - useAsyncApi, - this.Connection, - updatedEntity, - null, - TestContext.Current.CancellationToken - )) - .Should().Be(1); + (await this.CallApi(useAsyncApi, this.Connection, updatedEntity, null, TestContext.Current.CancellationToken)) + .Should() + .Be(1); - (await this.Connection.QuerySingleAsync( + ( + await this.Connection.QuerySingleAsync( $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(updatedEntity); + ) + ) + .Should() + .BeEquivalentTo(updatedEntity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task UpdateEntity_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task UpdateEntity_Transaction_ShouldUseTransaction(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -396,27 +385,32 @@ public async Task UpdateEntity_Transaction_ShouldUseTransaction(Boolean useAsync { var updatedEntity = Generate.UpdateFor(entity); - (await this.CallApi( + ( + await this.CallApi( useAsyncApi, this.Connection, updatedEntity, transaction, TestContext.Current.CancellationToken - )) - .Should().Be(1); + ) + ) + .Should() + .Be(1); (await this.Connection.QuerySingleAsync($"SELECT * FROM {Q("Entity")}", transaction)) - .Should().BeEquivalentTo(updatedEntity); + .Should() + .BeEquivalentTo(updatedEntity); await transaction.RollbackAsync(); } (await this.Connection.QuerySingleAsync($"SELECT * FROM {Q("Entity")}")) - .Should().BeEquivalentTo(entity); + .Should() + .BeEquivalentTo(entity); } - private Task CallApi( - Boolean useAsyncApi, + private Task CallApi( + bool useAsyncApi, DbConnection connection, TEntity entity, DbTransaction? transaction = null, @@ -431,15 +425,11 @@ private Task CallApi( try { - return Task.FromResult( - this.manipulator.UpdateEntity(connection, entity, transaction, cancellationToken) - ); + return Task.FromResult(this.manipulator.UpdateEntity(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/MySql/MySqlDatabaseAdapterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs index afa212b..fd23e97 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs @@ -9,7 +9,6 @@ public void SupportsTemporaryTables_ShouldReturnTrue() { var adapter = new MySqlDatabaseAdapter(); - adapter.SupportsTemporaryTables(this.Connection) - .Should().BeTrue(); + adapter.SupportsTemporaryTables(this.Connection).Should().BeTrue(); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs index 9876357..624c913 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs @@ -7,15 +7,16 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters.Orac public class OracleDatabaseAdapterTests : IntegrationTestsBase { + private readonly OracleDatabaseAdapter adapter = new(); + [Fact] public void QuoteTemporaryTableName_ShouldQuoteTableName() { - var prefix = this.Connection.ExecuteScalar( + var prefix = this.Connection.ExecuteScalar( "SELECT VALUE FROM v$parameter WHERE NAME = 'private_temp_table_prefix'" ); - this.adapter.QuoteTemporaryTableName("TempTable", this.Connection) - .Should().Be($"\"{prefix}TempTable\""); + this.adapter.QuoteTemporaryTableName("TempTable", this.Connection).Should().Be($"\"{prefix}TempTable\""); } [Fact] @@ -26,14 +27,14 @@ public void WasSqlStatementCancelledByCancellationToken_StatementWasCancelled_Sh var cancellationToken = CreateCancellationTokenThatIsCancelledAfter100Milliseconds(); - using var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); + using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + command, + cancellationToken + ); - var exception = Invoking(() => command.ExecuteNonQuery()) - .Should().Throw().Subject.First(); + var exception = Invoking(() => command.ExecuteNonQuery()).Should().Throw().Subject.First(); - this.adapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) - .Should().BeTrue(); + this.adapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken).Should().BeTrue(); } [Fact] @@ -42,12 +43,8 @@ public void WasSqlStatementCancelledByCancellationToken_StatementWasNotCancelled using var command = this.Connection.CreateCommand(); command.CommandText = "InvalidStatement"; - var exception = Invoking(() => command.ExecuteNonQuery()) - .Should().Throw().Subject.First(); + var exception = Invoking(() => command.ExecuteNonQuery()).Should().Throw().Subject.First(); - this.adapter.WasSqlStatementCancelledByCancellationToken(exception, CancellationToken.None) - .Should().BeFalse(); + this.adapter.WasSqlStatementCancelledByCancellationToken(exception, CancellationToken.None).Should().BeFalse(); } - - private readonly OracleDatabaseAdapter adapter = new(); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs index 04800ab..496c00b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs @@ -7,10 +7,11 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters.Post public class PostgreSqlDatabaseAdapterTests : IntegrationTestsBase { + private readonly PostgreSqlDatabaseAdapter adapter = new(); + [Fact] public void SupportsTemporaryTables_ShouldReturnTrue() => - this.adapter.SupportsTemporaryTables(this.Connection) - .Should().BeTrue(); + this.adapter.SupportsTemporaryTables(this.Connection).Should().BeTrue(); [Fact] public void WasSqlStatementCancelledByCancellationToken_StatementWasCancelled_ShouldReturnTrue() @@ -23,10 +24,11 @@ public void WasSqlStatementCancelledByCancellationToken_StatementWasCancelled_Sh using var registration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); var exception = Invoking(() => command.ExecuteNonQuery()) - .Should().Throw().Subject.First(); + .Should() + .Throw() + .Subject.First(); - this.adapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) - .Should().BeTrue(); + this.adapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken).Should().BeTrue(); } [Fact] @@ -35,12 +37,8 @@ public void WasSqlStatementCancelledByCancellationToken_StatementWasNotCancelled using var command = this.Connection.CreateCommand(); command.CommandText = "InvalidStatement"; - var exception = Invoking(() => command.ExecuteNonQuery()) - .Should().Throw().Subject.First(); + var exception = Invoking(() => command.ExecuteNonQuery()).Should().Throw().Subject.First(); - this.adapter.WasSqlStatementCancelledByCancellationToken(exception, CancellationToken.None) - .Should().BeFalse(); + this.adapter.WasSqlStatementCancelledByCancellationToken(exception, CancellationToken.None).Should().BeFalse(); } - - private readonly PostgreSqlDatabaseAdapter adapter = new(); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs index 88ea1b5..28ec8a1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs @@ -6,10 +6,11 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters.SqlS public class SqlServerDatabaseAdapterTests : IntegrationTestsBase { + private readonly SqlServerDatabaseAdapter adapter = new(); + [Fact] public void SupportsTemporaryTables_ShouldReturnTrue() => - this.adapter.SupportsTemporaryTables(this.Connection) - .Should().BeTrue(); + this.adapter.SupportsTemporaryTables(this.Connection).Should().BeTrue(); [Fact] public void WasSqlStatementCancelledByCancellationToken_StatementWasCancelled_ShouldReturnTrue() @@ -21,11 +22,9 @@ public void WasSqlStatementCancelledByCancellationToken_StatementWasCancelled_Sh using var registration = DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); - var exception = Invoking(() => command.ExecuteNonQuery()) - .Should().Throw().Subject.First(); + var exception = Invoking(() => command.ExecuteNonQuery()).Should().Throw().Subject.First(); - this.adapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) - .Should().BeTrue(); + this.adapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken).Should().BeTrue(); } [Fact] @@ -34,12 +33,8 @@ public void WasSqlStatementCancelledByCancellationToken_StatementWasNotCancelled using var command = this.Connection.CreateCommand(); command.CommandText = "InvalidStatement"; - var exception = Invoking(() => command.ExecuteNonQuery()) - .Should().Throw().Subject.First(); + var exception = Invoking(() => command.ExecuteNonQuery()).Should().Throw().Subject.First(); - this.adapter.WasSqlStatementCancelledByCancellationToken(exception, CancellationToken.None) - .Should().BeFalse(); + this.adapter.WasSqlStatementCancelledByCancellationToken(exception, CancellationToken.None).Should().BeFalse(); } - - private readonly SqlServerDatabaseAdapter adapter = new(); } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs index e80324d..d279be9 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs @@ -9,7 +9,6 @@ public void SupportsTemporaryTables_ShouldReturnTrue() { var adapter = new SqliteDatabaseAdapter(); - adapter.SupportsTemporaryTables(this.Connection) - .Should().BeTrue(); + adapter.SupportsTemporaryTables(this.Connection).Should().BeTrue(); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs index efb49f7..3e74837 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DatabaseAdapters/TemporaryTableBuilderTests.cs @@ -6,38 +6,29 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DatabaseAdapters; -public sealed class - TemporaryTableBuilderTests_MySql : - TemporaryTableBuilderTests; +public sealed class TemporaryTableBuilderTests_MySql : TemporaryTableBuilderTests; -public sealed class - TemporaryTableBuilderTests_Oracle : - TemporaryTableBuilderTests; +public sealed class TemporaryTableBuilderTests_Oracle : TemporaryTableBuilderTests; -public sealed class - TemporaryTableBuilderTests_PostgreSql : - TemporaryTableBuilderTests; +public sealed class TemporaryTableBuilderTests_PostgreSql : TemporaryTableBuilderTests; -public sealed class - TemporaryTableBuilderTests_Sqlite : - TemporaryTableBuilderTests; +public sealed class TemporaryTableBuilderTests_Sqlite : TemporaryTableBuilderTests; -public sealed class - TemporaryTableBuilderTests_SqlServer : - TemporaryTableBuilderTests; +public sealed class TemporaryTableBuilderTests_SqlServer : TemporaryTableBuilderTests; public abstract class TemporaryTableBuilderTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { + private readonly ITemporaryTableBuilder builder; + /// - protected TemporaryTableBuilderTests() => - this.builder = this.DatabaseAdapter.TemporaryTableBuilder; + protected TemporaryTableBuilderTests() => this.builder = this.DatabaseAdapter.TemporaryTableBuilder; [Theory] [InlineData(false)] [InlineData(true)] public async Task BuildTemporaryTable_ComplexObjects_DateTimeOffsetProperty_ShouldSupportDateTimeOffset( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); @@ -54,20 +45,24 @@ Boolean useAsyncApi TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(items); + ( + await this + .Connection.QueryAsync( + $"SELECT * FROM {QT("Objects")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(items); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -86,7 +81,8 @@ Boolean useAsyncApi if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) { this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum")); + .Should() + .StartWith(this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum")); } await using var reader = await this.Connection.ExecuteReaderAsync( @@ -94,25 +90,22 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - reader.GetFieldType(0) - .Should().BeAnyOf(typeof(Int32), typeof(Int64)); + reader.GetFieldType(0).Should().BeAnyOf(typeof(int), typeof(long)); foreach (var entity in entities) { await reader.ReadAsync(TestContext.Current.CancellationToken); - reader.GetInt32(0) - .Should().Be((Int32)entity.Enum); + reader.GetInt32(0).Should().Be((int)entity.Enum); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -131,7 +124,8 @@ Boolean useAsyncApi if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) { this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum")); + .Should() + .StartWith(this.GetDataTypeOfTemporaryTableColumn("Objects", "Enum")); } await using var reader = await this.Connection.ExecuteReaderAsync( @@ -139,25 +133,22 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - reader.GetFieldType(0) - .Should().Be(typeof(String)); + reader.GetFieldType(0).Should().Be(typeof(string)); foreach (var entity in entities) { await reader.ReadAsync(TestContext.Current.CancellationToken); - reader.GetString(0) - .Should().Be(entity.Enum.ToString()); + reader.GetString(0).Should().Be(entity.Enum.ToString()); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ComplexObjects_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( + bool useAsyncApi + ) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); @@ -175,16 +166,13 @@ Boolean useAsyncApi var columnCollation = this.GetCollationOfTemporaryTableColumn("Objects", "Enum"); - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); + columnCollation.Should().Be(this.TestDatabaseProvider.DatabaseCollation); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_Mapping_Attributes_ShouldUseAttributesMapping( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ComplexObjects_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entities = Generate.Multiple(); entities.ForEach(a => a.NotMapped = "ShouldNotBePersisted"); @@ -204,25 +192,25 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - reader.GetFieldNames() - .Should().NotContain(nameof(MappingTestEntityAttributes.NotMapped)); + reader.GetFieldNames().Should().NotContain(nameof(MappingTestEntityAttributes.NotMapped)); await reader.DisposeAsync(); this.Connection.Query($"SELECT * FROM {QT("Objects")}") - .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 BuildTemporaryTable_ComplexObjects_Mapping_FluentApi_ShouldUseFluentApiMapping( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ComplexObjects_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); @@ -244,16 +232,18 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - reader.GetFieldNames() - .Should().NotContain(nameof(MappingTestEntityFluentApi.NotMapped)); + reader.GetFieldNames().Should().NotContain(nameof(MappingTestEntityFluentApi.NotMapped)); await reader.DisposeAsync(); this.Connection.Query($"SELECT * FROM {QT("Objects")}") - .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")) ); } @@ -261,7 +251,7 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task BuildTemporaryTable_ComplexObjects_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( - Boolean useAsyncApi + bool useAsyncApi ) { var entities = Generate.Multiple(); @@ -276,14 +266,13 @@ Boolean useAsyncApi TestContext.Current.CancellationToken ); - this.Connection.Query($"SELECT * FROM {QT("Objects")}") - .Should().BeEquivalentTo(entities); + this.Connection.Query($"SELECT * FROM {QT("Objects")}").Should().BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_ShouldCreateMultiColumnTable(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_ShouldCreateMultiColumnTable(bool useAsyncApi) { var items = Generate.Multiple(); @@ -297,17 +286,22 @@ public async Task BuildTemporaryTable_ComplexObjects_ShouldCreateMultiColumnTabl TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(items); + ( + await this + .Connection.QueryAsync( + $"SELECT * FROM {QT("Objects")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(items); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabaseForTextColumns(bool useAsyncApi) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); @@ -323,14 +317,13 @@ public async Task BuildTemporaryTable_ComplexObjects_ShouldUseCollationOfDatabas var columnCollation = this.GetCollationOfTemporaryTableColumn("Objects", "StringValue"); - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); + columnCollation.Should().Be(this.TestDatabaseProvider.DatabaseCollation); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ComplexObjects_WithNullables_ShouldHandleNullValues(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ComplexObjects_WithNullables_ShouldHandleNullValues(bool useAsyncApi) { var itemsWithNulls = new List { new() }; @@ -344,18 +337,52 @@ public async Task BuildTemporaryTable_ComplexObjects_WithNullables_ShouldHandleN TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( - $"SELECT * FROM {QT("Objects")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(itemsWithNulls); + ( + await this + .Connection.QueryAsync( + $"SELECT * FROM {QT("Objects")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(itemsWithNulls); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task BuildTemporaryTable_ScalarValuesWithNullValues_ShouldHandleNullValues(bool useAsyncApi) + { + var values = Generate.MultipleNullable(); + + await using var tableDisposer = await this.CallApi( + useAsyncApi, + this.Connection, + null, + "NullValues", + values, + typeof(int?), + TestContext.Current.CancellationToken + ); + + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Value")} FROM {QT("NullValues")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(values); } [Theory] [InlineData(false)] [InlineData(true)] public async Task BuildTemporaryTable_ScalarValues_DateTimeOffsetValues_ShouldSupportDateTimeOffset( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); @@ -372,20 +399,24 @@ Boolean useAsyncApi TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( - $"SELECT * FROM {QT("Values")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(values); + ( + await this + .Connection.QueryAsync( + $"SELECT * FROM {QT("Values")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(values); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildTemporaryTable_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ScalarValues_EnumSerializationModeIsIntegers_ShouldStoreEnumValuesAsIntegers( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -404,7 +435,8 @@ Boolean useAsyncApi if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) { this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Values", "Value")); + .Should() + .StartWith(this.GetDataTypeOfTemporaryTableColumn("Values", "Value")); } await using var reader = await this.Connection.ExecuteReaderAsync( @@ -412,25 +444,22 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - reader.GetFieldType(0) - .Should().BeAnyOf(typeof(Int32), typeof(Int64)); + reader.GetFieldType(0).Should().BeAnyOf(typeof(int), typeof(long)); foreach (var value in values) { await reader.ReadAsync(TestContext.Current.CancellationToken); - reader.GetInt32(0) - .Should().Be((Int32)value); + reader.GetInt32(0).Should().Be((int)value); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldStoreEnumValuesAsStrings( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -449,7 +478,8 @@ Boolean useAsyncApi if (this.TestDatabaseProvider.CanRetrieveStructureOfTemporaryTables) { this.DatabaseAdapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().StartWith(this.GetDataTypeOfTemporaryTableColumn("Values", "Value")); + .Should() + .StartWith(this.GetDataTypeOfTemporaryTableColumn("Values", "Value")); } await using var reader = await this.Connection.ExecuteReaderAsync( @@ -457,25 +487,22 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - reader.GetFieldType(0) - .Should().Be(typeof(String)); + reader.GetFieldType(0).Should().Be(typeof(string)); foreach (var value in values) { await reader.ReadAsync(TestContext.Current.CancellationToken); - reader.GetString(0) - .Should().Be(value.ToString()); + reader.GetString(0).Should().Be(value.ToString()); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( - Boolean useAsyncApi - ) + public async Task BuildTemporaryTable_ScalarValues_EnumSerializationModeIsStrings_ShouldUseCollationOfDatabaseForEnumColumns( + bool useAsyncApi + ) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); @@ -493,15 +520,15 @@ Boolean useAsyncApi var columnCollation = this.GetCollationOfTemporaryTableColumn("Values", "Value"); - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); + columnCollation.Should().Be(this.TestDatabaseProvider.DatabaseCollation); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildTemporaryTable_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ScalarValues_NullableEnumValues_ShouldFillTableWithEnumsAndNulls( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -517,19 +544,24 @@ public async Task TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( - $"SELECT {Q("Value")} FROM {QT("Values")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(values); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Value")} FROM {QT("Values")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(values); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ScalarValues_ShouldCreateSingleColumnTable(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ScalarValues_ShouldCreateSingleColumnTable(bool useAsyncApi) { - var values = Generate.Multiple(); + var values = Generate.Multiple(); await using var tableDisposer = await this.CallApi( useAsyncApi, @@ -537,21 +569,26 @@ public async Task BuildTemporaryTable_ScalarValues_ShouldCreateSingleColumnTable null, "Values", values, - typeof(Int32), + typeof(int), TestContext.Current.CancellationToken ); - (await this.Connection.QueryAsync( - $"SELECT {Q("Value")} FROM {QT("Values")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(values); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Value")} FROM {QT("Values")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(values); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ScalarValues_ShouldUseCollationOfDatabaseForTextColumns(bool useAsyncApi) { Assert.SkipWhen(this.TestDatabaseProvider.TemporaryTableTextColumnInheritsCollationFromDatabase, ""); @@ -560,70 +597,43 @@ public async Task BuildTemporaryTable_ScalarValues_ShouldUseCollationOfDatabaseF this.Connection, null, "Values", - Generate.Multiple(), - typeof(String), + Generate.Multiple(), + typeof(string), TestContext.Current.CancellationToken ); var columnCollation = this.GetCollationOfTemporaryTableColumn("Values", "Value"); - columnCollation - .Should().Be(this.TestDatabaseProvider.DatabaseCollation); + columnCollation.Should().Be(this.TestDatabaseProvider.DatabaseCollation); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildTemporaryTable_ScalarValuesWithNullValues_ShouldHandleNullValues(Boolean useAsyncApi) - { - var values = Generate.MultipleNullable(); - - await using var tableDisposer = await this.CallApi( - useAsyncApi, - this.Connection, - null, - "NullValues", - values, - typeof(Int32?), - TestContext.Current.CancellationToken - ); - - (await this.Connection.QueryAsync( - $"SELECT {Q("Value")} FROM {QT("NullValues")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(values); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task BuildTemporaryTable_ShouldReturnDisposerThatDropsTableAsync(Boolean useAsyncApi) + public async Task BuildTemporaryTable_ShouldReturnDisposerThatDropsTableAsync(bool useAsyncApi) { var disposer = await this.CallApi( useAsyncApi, this.Connection, null, "Values", - Generate.Multiple(), - typeof(Int32), + Generate.Multiple(), + typeof(int), TestContext.Current.CancellationToken ); - this.ExistsTemporaryTableInDb("Values") - .Should().BeTrue(); + this.ExistsTemporaryTableInDb("Values").Should().BeTrue(); await disposer.DisposeAsync(); - this.ExistsTemporaryTableInDb("Values") - .Should().BeFalse(); + this.ExistsTemporaryTableInDb("Values").Should().BeFalse(); } private Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, DbTransaction? transaction, - String name, + string name, IEnumerable values, Type valuesType, CancellationToken cancellationToken = default @@ -652,6 +662,4 @@ private Task CallApi( return Task.FromException(ex); } } - - private readonly ITemporaryTableBuilder builder; } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs index 243ad8d..904a28b 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandBuilderTests.cs @@ -4,25 +4,15 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DbCommands; -public sealed class - DbCommandBuilderTests_MySql : - DbCommandBuilderTests; +public sealed class DbCommandBuilderTests_MySql : DbCommandBuilderTests; -public sealed class - DbCommandBuilderTests_Oracle : - DbCommandBuilderTests; +public sealed class DbCommandBuilderTests_Oracle : DbCommandBuilderTests; -public sealed class - DbCommandBuilderTests_PostgreSql : - DbCommandBuilderTests; +public sealed class DbCommandBuilderTests_PostgreSql : DbCommandBuilderTests; -public sealed class - DbCommandBuilderTests_Sqlite : - DbCommandBuilderTests; +public sealed class DbCommandBuilderTests_Sqlite : DbCommandBuilderTests; -public sealed class - DbCommandBuilderTests_SqlServer : - DbCommandBuilderTests; +public sealed class DbCommandBuilderTests_SqlServer : DbCommandBuilderTests; public abstract class DbCommandBuilderTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() @@ -30,7 +20,7 @@ public abstract class DbCommandBuilderTests : Integration [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldCreateTemporaryTables(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldCreateTemporaryTables(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -38,45 +28,52 @@ public async Task BuildDbCommand_ShouldCreateTemporaryTables(Boolean useAsyncApi var entities = Generate.Multiple(); InterpolatedSqlStatement statement = $""" - SELECT Value - FROM {TemporaryTable(entityIds)} AS Ids - INNER JOIN {TemporaryTable(entities)} AS Entities - ON Entities.Id = Ids.Value - """; + SELECT Value + FROM {TemporaryTable(entityIds)} AS Ids + INNER JOIN {TemporaryTable(entities)} AS Entities + ON Entities.Id = Ids.Value + """; var (command, _) = await CallApi(useAsyncApi, statement, this.DatabaseAdapter, this.Connection); var temporaryTables = statement.TemporaryTables; - command.CommandText - .Should().Be( + command + .CommandText.Should() + .Be( $""" - SELECT Value - FROM {QT(temporaryTables[0].Name)} AS Ids - INNER JOIN {QT(temporaryTables[1].Name)} AS Entities - ON Entities.Id = Ids.Value - """ + SELECT Value + FROM {QT(temporaryTables[0].Name)} AS Ids + INNER JOIN {QT(temporaryTables[1].Name)} AS Entities + ON Entities.Id = Ids.Value + """ ); - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeTrue(); - - this.ExistsTemporaryTableInDb(temporaryTables[1].Name) - .Should().BeTrue(); - - (await this.Connection.QueryAsync($"SELECT {Q("Value")} FROM {QT(temporaryTables[0].Name)}") - .ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entityIds); - - (await this.Connection.QueryAsync($"SELECT * FROM {QT(temporaryTables[1].Name)}") - .ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeTrue(); + + this.ExistsTemporaryTableInDb(temporaryTables[1].Name).Should().BeTrue(); + + ( + await this + .Connection.QueryAsync($"SELECT {Q("Value")} FROM {QT(temporaryTables[0].Name)}") + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entityIds); + + ( + await this + .Connection.QueryAsync($"SELECT * FROM {QT(temporaryTables[1].Name)}") + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldReturnDisposerForCommandWhichDisposesTemporaryTables(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldReturnDisposerForCommandWhichDisposesTemporaryTables(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -84,58 +81,45 @@ public async Task BuildDbCommand_ShouldReturnDisposerForCommandWhichDisposesTemp var entities = Generate.Multiple(); InterpolatedSqlStatement statement = $""" - SELECT Value - FROM {TemporaryTable(entityIds)} AS Ids - INNER JOIN {TemporaryTable(entities)} AS Entities - ON Entities.Id = Ids.Value - """; - var (_, commandDisposer) = - await CallApi(useAsyncApi, statement, this.DatabaseAdapter, this.Connection); + SELECT Value + FROM {TemporaryTable(entityIds)} AS Ids + INNER JOIN {TemporaryTable(entities)} AS Entities + ON Entities.Id = Ids.Value + """; + var (_, commandDisposer) = await CallApi(useAsyncApi, statement, this.DatabaseAdapter, this.Connection); var temporaryTables = statement.TemporaryTables; var table1Name = temporaryTables[0].Name; var table2Name = temporaryTables[1].Name; - this.ExistsTemporaryTableInDb(table1Name) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(table1Name).Should().BeTrue(); - this.ExistsTemporaryTableInDb(table2Name) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(table2Name).Should().BeTrue(); await commandDisposer.DisposeAsync(); - this.ExistsTemporaryTableInDb(table1Name) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(table1Name).Should().BeFalse(); - this.ExistsTemporaryTableInDb(table2Name) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(table2Name).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldSetCommandTimeout(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldSetCommandTimeout(bool useAsyncApi) { var timeout = Generate.Single(); - var (command, _) = await CallApi( - useAsyncApi, - "SELECT 1", - this.DatabaseAdapter, - this.Connection, - null, - timeout - ); + var (command, _) = await CallApi(useAsyncApi, "SELECT 1", this.DatabaseAdapter, this.Connection, null, timeout); - command.CommandTimeout - .Should().Be((Int32)timeout.TotalSeconds); + command.CommandTimeout.Should().Be((int)timeout.TotalSeconds); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldSetCommandType(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldSetCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProcedures, ""); @@ -147,91 +131,78 @@ public async Task BuildDbCommand_ShouldSetCommandType(Boolean useAsyncApi) commandType: CommandType.StoredProcedure ); - command.CommandType - .Should().Be(CommandType.StoredProcedure); + command.CommandType.Should().Be(CommandType.StoredProcedure); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldSetConnection(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldSetConnection(bool useAsyncApi) { - var (command, _) = - await CallApi(useAsyncApi, "SELECT 1", this.DatabaseAdapter, this.Connection); + var (command, _) = await CallApi(useAsyncApi, "SELECT 1", this.DatabaseAdapter, this.Connection); - command.Connection - .Should().BeSameAs(this.Connection); + command.Connection.Should().BeSameAs(this.Connection); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldSetParameters(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldSetParameters(bool useAsyncApi) { var entityId = Generate.Id(); var dateTimeValue = DateTime.UtcNow; - var stringValue = Generate.Single(); + var stringValue = Generate.Single(); var (command, _) = await CallApi( useAsyncApi, $""" - SELECT * - FROM Entity - WHERE Id = {Parameter(entityId)} AND - DateTimeValue = {Parameter(dateTimeValue)} AND - StringValue = {Parameter(stringValue)} - """, + SELECT * + FROM Entity + WHERE Id = {Parameter(entityId)} AND + DateTimeValue = {Parameter(dateTimeValue)} AND + StringValue = {Parameter(stringValue)} + """, this.DatabaseAdapter, this.Connection ); - command.CommandText - .Should().Be( + command + .CommandText.Should() + .Be( $""" - SELECT * - FROM Entity - WHERE Id = {P("EntityId")} AND - DateTimeValue = {P("DateTimeValue")} AND - StringValue = {P("StringValue")} - """ + SELECT * + FROM Entity + WHERE Id = {P("EntityId")} AND + DateTimeValue = {P("DateTimeValue")} AND + StringValue = {P("StringValue")} + """ ); - command.Parameters.Count - .Should().Be(3); + command.Parameters.Count.Should().Be(3); - command.Parameters["EntityId"].Value - .Should().Be(entityId); + command.Parameters["EntityId"].Value.Should().Be(entityId); - command.Parameters["DateTimeValue"].Value - .Should().Be(dateTimeValue); + command.Parameters["DateTimeValue"].Value.Should().Be(dateTimeValue); - command.Parameters["StringValue"].Value - .Should().Be(stringValue); + command.Parameters["StringValue"].Value.Should().Be(stringValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldSetTransaction(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldSetTransaction(bool useAsyncApi) { await using var transaction = await this.Connection.BeginTransactionAsync(); - var (command, _) = await CallApi( - useAsyncApi, - "SELECT 1", - this.DatabaseAdapter, - this.Connection, - transaction - ); + var (command, _) = await CallApi(useAsyncApi, "SELECT 1", this.DatabaseAdapter, this.Connection, transaction); - command.Transaction - .Should().BeSameAs(transaction); + command.Transaction.Should().BeSameAs(transaction); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldUseCancellationToken(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldUseCancellationToken(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -248,15 +219,17 @@ public async Task BuildDbCommand_ShouldUseCancellationToken(Boolean useAsyncApi) cancellationToken ); - var exception = (await Invoking(() => command.ExecuteNonQueryAsync(cancellationToken)) - .Should().ThrowAsync()).Subject.First(); + var exception = ( + await Invoking(() => command.ExecuteNonQueryAsync(cancellationToken)).Should().ThrowAsync() + ).Subject.First(); this.DatabaseAdapter.WasSqlStatementCancelledByCancellationToken(exception, cancellationToken) - .Should().BeTrue(); + .Should() + .BeTrue(); } private static Task<(DbCommand, DbCommandDisposer)> CallApi( - Boolean useAsyncApi, + bool useAsyncApi, InterpolatedSqlStatement statement, IDatabaseAdapter databaseAdapter, DbConnection connection, diff --git a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandDisposerTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandDisposerTests.cs index 7a39e24..d975e6e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandDisposerTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandDisposerTests.cs @@ -1,150 +1,128 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DbCommands; -public sealed class - DbCommandDisposerTests_MySql : - DbCommandDisposerTests; +public sealed class DbCommandDisposerTests_MySql : DbCommandDisposerTests; -public sealed class - DbCommandDisposerTests_Oracle : - DbCommandDisposerTests; +public sealed class DbCommandDisposerTests_Oracle : DbCommandDisposerTests; -public sealed class - DbCommandDisposerTests_PostgreSql : - DbCommandDisposerTests; +public sealed class DbCommandDisposerTests_PostgreSql : DbCommandDisposerTests; -public sealed class - DbCommandDisposerTests_Sqlite : - DbCommandDisposerTests; +public sealed class DbCommandDisposerTests_Sqlite : DbCommandDisposerTests; -public sealed class - DbCommandDisposerTests_SqlServer : - DbCommandDisposerTests; +public sealed class DbCommandDisposerTests_SqlServer : DbCommandDisposerTests; public abstract class DbCommandDisposerTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Fact] - public void Dispose_AlreadyDisposed_ShouldNotAttemptToDropTemporaryTablesAgain() + public async Task DisposeAsync_AlreadyDisposed_ShouldNotAttemptToDropTemporaryTablesAgain() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(); - InterpolatedSqlStatement statement = $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}"; var temporaryTables = statement.TemporaryTables; - var (_, commandDisposer) = DbCommandBuilder.BuildDbCommand(statement, this.DatabaseAdapter, this.Connection); + var (_, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( + statement, + this.DatabaseAdapter, + this.Connection + ); - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeTrue(); - commandDisposer.Dispose(); + await commandDisposer.DisposeAsync(); - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeFalse(); - Invoking(() => commandDisposer.Dispose()) - .Should().NotThrow(); + await Invoking(() => commandDisposer.DisposeAsync().AsTask()).Should().NotThrowAsync(); - Invoking(() => commandDisposer.Dispose()) - .Should().NotThrow(); + await Invoking(() => commandDisposer.DisposeAsync().AsTask()).Should().NotThrowAsync(); - Invoking(() => commandDisposer.Dispose()) - .Should().NotThrow(); + await Invoking(() => commandDisposer.DisposeAsync().AsTask()).Should().NotThrowAsync(); } [Fact] - public void Dispose_ShouldDropTemporaryTables() + public async Task DisposeAsync_ShouldDisposeTemporaryTables() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds1 = Generate.Ids(); var entityIds2 = Generate.Ids(); - InterpolatedSqlStatement statement = - $""" - SELECT {Q("Value")} FROM {TemporaryTable(entityIds1)} - UNION - SELECT {Q("Value")} FROM {TemporaryTable(entityIds2)} - """; + InterpolatedSqlStatement statement = $""" + SELECT {Q("Value")} FROM {TemporaryTable(entityIds1)} + UNION + SELECT {Q("Value")} FROM {TemporaryTable(entityIds2)} + """; var temporaryTables = statement.TemporaryTables; - var (_, commandDisposer) = DbCommandBuilder.BuildDbCommand(statement, this.DatabaseAdapter, this.Connection); - - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeTrue(); + var (_, commandDisposer) = await DbCommandBuilder.BuildDbCommandAsync( + statement, + this.DatabaseAdapter, + this.Connection + ); - this.ExistsTemporaryTableInDb(temporaryTables[1].Name) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeTrue(); - commandDisposer.Dispose(); - - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeFalse(); + await commandDisposer.DisposeAsync(); - this.ExistsTemporaryTableInDb(temporaryTables[1].Name) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeFalse(); } [Fact] - public async Task DisposeAsync_AlreadyDisposed_ShouldNotAttemptToDropTemporaryTablesAgain() + public void Dispose_AlreadyDisposed_ShouldNotAttemptToDropTemporaryTablesAgain() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(); + InterpolatedSqlStatement statement = $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}"; var temporaryTables = statement.TemporaryTables; - var (_, commandDisposer) = - await DbCommandBuilder.BuildDbCommandAsync(statement, this.DatabaseAdapter, this.Connection); + var (_, commandDisposer) = DbCommandBuilder.BuildDbCommand(statement, this.DatabaseAdapter, this.Connection); - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeTrue(); - await commandDisposer.DisposeAsync(); + commandDisposer.Dispose(); - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeFalse(); - await Invoking(() => commandDisposer.DisposeAsync().AsTask()) - .Should().NotThrowAsync(); + Invoking(() => commandDisposer.Dispose()).Should().NotThrow(); - await Invoking(() => commandDisposer.DisposeAsync().AsTask()) - .Should().NotThrowAsync(); + Invoking(() => commandDisposer.Dispose()).Should().NotThrow(); - await Invoking(() => commandDisposer.DisposeAsync().AsTask()) - .Should().NotThrowAsync(); + Invoking(() => commandDisposer.Dispose()).Should().NotThrow(); } [Fact] - public async Task DisposeAsync_ShouldDisposeTemporaryTables() + public void Dispose_ShouldDropTemporaryTables() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds1 = Generate.Ids(); var entityIds2 = Generate.Ids(); - InterpolatedSqlStatement statement = - $""" - SELECT {Q("Value")} FROM {TemporaryTable(entityIds1)} - UNION - SELECT {Q("Value")} FROM {TemporaryTable(entityIds2)} - """; + InterpolatedSqlStatement statement = $""" + SELECT {Q("Value")} FROM {TemporaryTable(entityIds1)} + UNION + SELECT {Q("Value")} FROM {TemporaryTable(entityIds2)} + """; var temporaryTables = statement.TemporaryTables; - var (_, commandDisposer) = - await DbCommandBuilder.BuildDbCommandAsync(statement, this.DatabaseAdapter, this.Connection); + var (_, commandDisposer) = DbCommandBuilder.BuildDbCommand(statement, this.DatabaseAdapter, this.Connection); - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeTrue(); - await commandDisposer.DisposeAsync(); + this.ExistsTemporaryTableInDb(temporaryTables[1].Name).Should().BeTrue(); + + commandDisposer.Dispose(); + + this.ExistsTemporaryTableInDb(temporaryTables[0].Name).Should().BeFalse(); - this.ExistsTemporaryTableInDb(temporaryTables[0].Name) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTables[1].Name).Should().BeFalse(); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandHelperTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandHelperTests.cs index a23ee73..f21f6fa 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandHelperTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbCommands/DbCommandHelperTests.cs @@ -1,24 +1,14 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.DbCommands; -public sealed class - DbCommandHelperTests_MySql : - DbCommandHelperTests; +public sealed class DbCommandHelperTests_MySql : DbCommandHelperTests; -public sealed class - DbCommandHelperTests_Oracle : - DbCommandHelperTests; +public sealed class DbCommandHelperTests_Oracle : DbCommandHelperTests; -public sealed class - DbCommandHelperTests_PostgreSql : - DbCommandHelperTests; +public sealed class DbCommandHelperTests_PostgreSql : DbCommandHelperTests; -public sealed class - DbCommandHelperTests_Sqlite : - DbCommandHelperTests; +public sealed class DbCommandHelperTests_Sqlite : DbCommandHelperTests; -public sealed class - DbCommandHelperTests_SqlServer : - DbCommandHelperTests; +public sealed class DbCommandHelperTests_SqlServer : DbCommandHelperTests; public abstract class DbCommandHelperTests : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() @@ -34,17 +24,16 @@ public void RegisterDbCommandCancellation_CancellationToken_ShouldRegister() var command = this.Connection.CreateCommand(); - command.CommandText = - this.TestDatabaseProvider.DelayTwoSecondsStatement + $"DELETE FROM {Q("Entity")}"; + command.CommandText = this.TestDatabaseProvider.DelayTwoSecondsStatement + $"DELETE FROM {Q("Entity")}"; - using var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(command, cancellationToken); + using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + command, + cancellationToken + ); - Invoking(() => command.ExecuteNonQuery()) - .Should().Throw(); + Invoking(() => command.ExecuteNonQuery()).Should().Throw(); - this.ExistsEntityInDb(entity) - .Should().BeTrue(); + this.ExistsEntityInDb(entity).Should().BeTrue(); } [Fact] @@ -56,16 +45,15 @@ public void RegisterDbCommandCancellation_NoneCancellationToken_ShouldNotRegiste var command = this.Connection.CreateCommand(); - command.CommandText = - this.TestDatabaseProvider.DelayTwoSecondsStatement + $"DELETE FROM {Q("Entity")}"; + command.CommandText = this.TestDatabaseProvider.DelayTwoSecondsStatement + $"DELETE FROM {Q("Entity")}"; - using var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(command, CancellationToken.None); + using var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + command, + CancellationToken.None + ); - command.ExecuteNonQuery() - .Should().Be(1); + command.ExecuteNonQuery().Should().Be(1); - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entity).Should().BeFalse(); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs index 8c07bea..49e546f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteNonQueryTests.cs @@ -2,36 +2,29 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_ExecuteNonQueryTests_MySql : - DbConnectionExtensions_ExecuteNonQueryTests; +public sealed class DbConnectionExtensions_ExecuteNonQueryTests_MySql + : DbConnectionExtensions_ExecuteNonQueryTests; -public sealed class - DbConnectionExtensions_ExecuteNonQueryTests_Oracle : - DbConnectionExtensions_ExecuteNonQueryTests; +public sealed class DbConnectionExtensions_ExecuteNonQueryTests_Oracle + : DbConnectionExtensions_ExecuteNonQueryTests; -public sealed class - DbConnectionExtensions_ExecuteNonQueryTests_PostgreSql : - DbConnectionExtensions_ExecuteNonQueryTests; +public sealed class DbConnectionExtensions_ExecuteNonQueryTests_PostgreSql + : DbConnectionExtensions_ExecuteNonQueryTests; -public sealed class - DbConnectionExtensions_ExecuteNonQueryTests_Sqlite : - DbConnectionExtensions_ExecuteNonQueryTests; +public sealed class DbConnectionExtensions_ExecuteNonQueryTests_Sqlite + : DbConnectionExtensions_ExecuteNonQueryTests; -public sealed class - DbConnectionExtensions_ExecuteNonQueryTests_SqlServer : - DbConnectionExtensions_ExecuteNonQueryTests; +public sealed class DbConnectionExtensions_ExecuteNonQueryTests_SqlServer + : DbConnectionExtensions_ExecuteNonQueryTests; -public abstract class - DbConnectionExtensions_ExecuteNonQueryTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_ExecuteNonQueryTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteNonQuery_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task ExecuteNonQuery_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -41,25 +34,26 @@ Boolean useAsyncApi this.DelayNextDbCommand = true; - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"DELETE FROM {Q("Entity")}", cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); // Since the operation was cancelled, the entity should still exist. - this.ExistsEntityInDb(entity) - .Should().BeTrue(); + this.ExistsEntityInDb(entity).Should().BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteNonQuery_CommandType_ShouldPassUseCommandType(Boolean useAsyncApi) + public async Task ExecuteNonQuery_CommandType_ShouldPassUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProcedures, ""); @@ -73,15 +67,14 @@ await CallApi( cancellationToken: TestContext.Current.CancellationToken ); - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entity).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] public async Task ExecuteNonQuery_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -89,39 +82,39 @@ Boolean useAsyncApi var entities = this.CreateEntitiesInDb(5); var entitiesToDelete = entities.Take(2).ToList(); - InterpolatedSqlStatement statement = - $""" - DELETE FROM {Q("Entity")} - WHERE EXISTS ( - SELECT 1 - FROM {TemporaryTable(entitiesToDelete)} TEntitiesToDelete - WHERE {Q("Entity")}.{Q("Id")} = TEntitiesToDelete.{Q("Id")} AND - {Q("Entity")}.{Q("StringValue")} = TEntitiesToDelete.{Q("StringValue")} AND - {Q("Entity")}.{Q("Int32Value")} = TEntitiesToDelete.{Q("Int32Value")} - ) - """; + InterpolatedSqlStatement statement = $""" + DELETE FROM {Q("Entity")} + WHERE EXISTS ( + SELECT 1 + FROM {TemporaryTable(entitiesToDelete)} TEntitiesToDelete + WHERE {Q("Entity")}.{Q("Id")} = TEntitiesToDelete.{Q("Id")} AND + {Q("Entity")}.{Q("StringValue")} = TEntitiesToDelete.{Q("StringValue")} AND + {Q("Entity")}.{Q("Int32Value")} = TEntitiesToDelete.{Q("Int32Value")} + ) + """; var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entitiesToDelete.Count); + ) + ) + .Should() + .Be(entitiesToDelete.Count); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - ExecuteNonQuery_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task ExecuteNonQuery_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -132,35 +125,33 @@ await CallApi( useAsyncApi, this.Connection, $""" - DELETE FROM {Q("Entity")} - WHERE EXISTS ( - SELECT 1 - FROM {TemporaryTable(entitiesToDelete)} TEntitiesToDelete - WHERE {Q("Entity")}.{Q("Id")} = TEntitiesToDelete.{Q("Id")} AND - {Q("Entity")}.{Q("StringValue")} = TEntitiesToDelete.{Q("StringValue")} AND - {Q("Entity")}.{Q("Int32Value")} = TEntitiesToDelete.{Q("Int32Value")} - ) - """, + DELETE FROM {Q("Entity")} + WHERE EXISTS ( + SELECT 1 + FROM {TemporaryTable(entitiesToDelete)} TEntitiesToDelete + WHERE {Q("Entity")}.{Q("Id")} = TEntitiesToDelete.{Q("Id")} AND + {Q("Entity")}.{Q("StringValue")} = TEntitiesToDelete.{Q("StringValue")} AND + {Q("Entity")}.{Q("Int32Value")} = TEntitiesToDelete.{Q("Int32Value")} + ) + """, cancellationToken: TestContext.Current.CancellationToken ); foreach (var entity in entitiesToDelete) { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entity).Should().BeFalse(); } foreach (var entity in entities.Except(entitiesToDelete)) { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); + this.ExistsEntityInDb(entity).Should().BeTrue(); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteNonQuery_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task ExecuteNonQuery_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -171,14 +162,13 @@ await CallApi( cancellationToken: TestContext.Current.CancellationToken ); - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entity).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteNonQuery_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task ExecuteNonQuery_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -194,15 +184,14 @@ await CallApi( cancellationToken: TestContext.Current.CancellationToken ); - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entity).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] public async Task ExecuteNonQuery_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -211,33 +200,33 @@ Boolean useAsyncApi var entitiesToDelete = entities.Take(2).ToList(); var idsOfEntitiesToDelete = entitiesToDelete.ConvertAll(a => a.Id); - InterpolatedSqlStatement statement = - $""" - DELETE FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(idsOfEntitiesToDelete)}) - """; + InterpolatedSqlStatement statement = $""" + DELETE FROM {Q("Entity")} + WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(idsOfEntitiesToDelete)}) + """; var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(idsOfEntitiesToDelete.Count); + ) + ) + .Should() + .Be(idsOfEntitiesToDelete.Count); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - ExecuteNonQuery_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task ExecuteNonQuery_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -249,53 +238,57 @@ await CallApi( useAsyncApi, this.Connection, $""" - DELETE FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(idsOfEntitiesToDelete)}) - """, + DELETE FROM {Q("Entity")} + WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(idsOfEntitiesToDelete)}) + """, cancellationToken: TestContext.Current.CancellationToken ); foreach (var entity in entitiesToDelete) { - this.ExistsEntityInDb(entity) - .Should().BeFalse(); + this.ExistsEntityInDb(entity).Should().BeFalse(); } foreach (var entity in entities.Except(entitiesToDelete)) { - this.ExistsEntityInDb(entity) - .Should().BeTrue(); + this.ExistsEntityInDb(entity).Should().BeTrue(); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteNonQuery_ShouldReturnNumberOfAffectedRows(Boolean useAsyncApi) + public async Task ExecuteNonQuery_ShouldReturnNumberOfAffectedRows(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(1); + ) + ) + .Should() + .Be(1); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"DELETE FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(0); + ) + ) + .Should() + .Be(0); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteNonQuery_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task ExecuteNonQuery_Transaction_ShouldUseTransaction(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -309,18 +302,16 @@ await CallApi( cancellationToken: TestContext.Current.CancellationToken ); - this.ExistsEntityInDb(entity, transaction) - .Should().BeFalse(); + this.ExistsEntityInDb(entity, transaction).Should().BeFalse(); await transaction.RollbackAsync(); } - this.ExistsEntityInDb(entity) - .Should().BeTrue(); + this.ExistsEntityInDb(entity).Should().BeTrue(); } - private static Task CallApi( - Boolean useAsyncApi, + private static Task CallApi( + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -348,7 +339,7 @@ private static Task CallApi( } catch (Exception ex) { - return Task.FromException(ex); + return Task.FromException(ex); } } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs index 0399f4f..e2dbf5d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteReaderTests.cs @@ -2,36 +2,29 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_ExecuteReaderTests_MySql : - DbConnectionExtensions_ExecuteReaderTests; +public sealed class DbConnectionExtensions_ExecuteReaderTests_MySql + : DbConnectionExtensions_ExecuteReaderTests; -public sealed class - DbConnectionExtensions_ExecuteReaderTests_Oracle : - DbConnectionExtensions_ExecuteReaderTests; +public sealed class DbConnectionExtensions_ExecuteReaderTests_Oracle + : DbConnectionExtensions_ExecuteReaderTests; -public sealed class - DbConnectionExtensions_ExecuteReaderTests_PostgreSql : - DbConnectionExtensions_ExecuteReaderTests; +public sealed class DbConnectionExtensions_ExecuteReaderTests_PostgreSql + : DbConnectionExtensions_ExecuteReaderTests; -public sealed class - DbConnectionExtensions_ExecuteReaderTests_Sqlite : - DbConnectionExtensions_ExecuteReaderTests; +public sealed class DbConnectionExtensions_ExecuteReaderTests_Sqlite + : DbConnectionExtensions_ExecuteReaderTests; -public sealed class - DbConnectionExtensions_ExecuteReaderTests_SqlServer : - DbConnectionExtensions_ExecuteReaderTests; +public sealed class DbConnectionExtensions_ExecuteReaderTests_SqlServer + : DbConnectionExtensions_ExecuteReaderTests; -public abstract class - DbConnectionExtensions_ExecuteReaderTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_ExecuteReaderTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteReader_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task ExecuteReader_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -40,23 +33,23 @@ Boolean useAsyncApi this.DelayNextDbCommand = true; await Invoking(async () => - { - await using var reader = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ); - } - ) - .Should().ThrowAsync() + { + await using var reader = await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: cancellationToken + ); + }) + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteReader_CommandBehavior_ShouldUseCommandBehavior(Boolean useAsyncApi) + public async Task ExecuteReader_CommandBehavior_ShouldUseCommandBehavior(bool useAsyncApi) { var reader = await CallApi( useAsyncApi, @@ -68,14 +61,13 @@ public async Task ExecuteReader_CommandBehavior_ShouldUseCommandBehavior(Boolean await reader.DisposeAsync(); - this.Connection.State - .Should().Be(ConnectionState.Closed); + this.Connection.State.Should().Be(ConnectionState.Closed); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteReader_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task ExecuteReader_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); @@ -91,31 +83,29 @@ public async Task ExecuteReader_CommandType_ShouldUseCommandType(Boolean useAsyn foreach (var entity in entities) { - (await reader.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeTrue(); + (await reader.ReadAsync(TestContext.Current.CancellationToken)).Should().BeTrue(); - reader.GetInt64(0) - .Should().Be(entity.Id); + reader.GetInt64(0).Should().Be(entity.Id); - reader.GetString(1) - .Should().Be(entity.StringValue); + reader.GetString(1).Should().Be(entity.StringValue); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - ExecuteReader_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal(Boolean useAsyncApi) + public async Task ExecuteReader_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(); InterpolatedSqlStatement statement = $""" - SELECT {Q("Id")} - FROM {TemporaryTable(entities)} - """; + SELECT {Q("Id")} + FROM {TemporaryTable(entities)} + """; var temporaryTableName = statement.TemporaryTables[0].Name; var reader = await CallApi( @@ -127,23 +117,20 @@ public async Task if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeTrue(); } await reader.DisposeAsync(); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - ExecuteReader_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task ExecuteReader_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -153,32 +140,28 @@ Boolean useAsyncApi useAsyncApi, this.Connection, $""" - SELECT {Q("Id")}, {Q("StringValue")}, {Q("DecimalValue")} - FROM {TemporaryTable(entities)} - """, + SELECT {Q("Id")}, {Q("StringValue")}, {Q("DecimalValue")} + FROM {TemporaryTable(entities)} + """, cancellationToken: TestContext.Current.CancellationToken ); foreach (var entity in entities) { - (await reader.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeTrue(); + (await reader.ReadAsync(TestContext.Current.CancellationToken)).Should().BeTrue(); - reader.GetInt64(0) - .Should().Be(entity.Id); + reader.GetInt64(0).Should().Be(entity.Id); - reader.GetString(1) - .Should().Be(entity.StringValue); + reader.GetString(1).Should().Be(entity.StringValue); - reader.GetDecimal(2) - .Should().Be(entity.DecimalValue); + reader.GetDecimal(2).Should().Be(entity.DecimalValue); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteReader_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task ExecuteReader_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -189,17 +172,15 @@ public async Task ExecuteReader_InterpolatedParameter_ShouldPassInterpolatedPara cancellationToken: TestContext.Current.CancellationToken ); - (await reader.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeTrue(); + (await reader.ReadAsync(TestContext.Current.CancellationToken)).Should().BeTrue(); - reader.GetString(0) - .Should().Be(entity.StringValue); + reader.GetString(0).Should().Be(entity.StringValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteReader_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task ExecuteReader_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -215,18 +196,16 @@ public async Task ExecuteReader_Parameter_ShouldPassParameter(Boolean useAsyncAp cancellationToken: TestContext.Current.CancellationToken ); - (await reader.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeTrue(); + (await reader.ReadAsync(TestContext.Current.CancellationToken)).Should().BeTrue(); - reader.GetString(0) - .Should().Be(entity.StringValue); + reader.GetString(0).Should().Be(entity.StringValue); } [Theory] [InlineData(false)] [InlineData(true)] public async Task ExecuteReader_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterDataReaderDisposal( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -246,23 +225,20 @@ Boolean useAsyncApi if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeTrue(); } await reader.DisposeAsync(); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - ExecuteReader_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task ExecuteReader_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -277,18 +253,16 @@ Boolean useAsyncApi foreach (var entityId in entityIds) { - (await reader.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeTrue(); + (await reader.ReadAsync(TestContext.Current.CancellationToken)).Should().BeTrue(); - reader.GetInt64(0) - .Should().Be(entityId); + reader.GetInt64(0).Should().Be(entityId); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteReader_ShouldReturnDataReaderForQueryResult(Boolean useAsyncApi) + public async Task ExecuteReader_ShouldReturnDataReaderForQueryResult(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); @@ -301,24 +275,20 @@ public async Task ExecuteReader_ShouldReturnDataReaderForQueryResult(Boolean use foreach (var entity in entities) { - (await reader.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeTrue(); + (await reader.ReadAsync(TestContext.Current.CancellationToken)).Should().BeTrue(); - reader.GetInt64(0) - .Should().Be(entity.Id); + reader.GetInt64(0).Should().Be(entity.Id); - reader.GetString(1) - .Should().Be(entity.StringValue); + reader.GetString(1).Should().Be(entity.StringValue); } - (await reader.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeFalse(); + (await reader.ReadAsync(TestContext.Current.CancellationToken)).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteReader_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task ExecuteReader_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { @@ -332,19 +302,15 @@ public async Task ExecuteReader_Transaction_ShouldUseTransaction(Boolean useAsyn cancellationToken: TestContext.Current.CancellationToken ); - reader.HasRows - .Should().BeTrue(); + reader.HasRows.Should().BeTrue(); foreach (var entity in entities) { - (await reader.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeTrue(); + (await reader.ReadAsync(TestContext.Current.CancellationToken)).Should().BeTrue(); - reader.GetInt64(0) - .Should().Be(entity.Id); + reader.GetInt64(0).Should().Be(entity.Id); - reader.GetString(1) - .Should().Be(entity.StringValue); + reader.GetString(1).Should().Be(entity.StringValue); } await reader.DisposeAsync(); @@ -352,17 +318,20 @@ public async Task ExecuteReader_Transaction_ShouldUseTransaction(Boolean useAsyn await transaction.RollbackAsync(); } - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )).HasRows - .Should().BeFalse(); + ) + ) + .HasRows.Should() + .BeFalse(); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs index 8d7baa5..c3735f8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExecuteScalarTests.cs @@ -2,36 +2,29 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_ExecuteScalarTests_MySql : - DbConnectionExtensions_ExecuteScalarTests; +public sealed class DbConnectionExtensions_ExecuteScalarTests_MySql + : DbConnectionExtensions_ExecuteScalarTests; -public sealed class - DbConnectionExtensions_ExecuteScalarTests_Oracle : - DbConnectionExtensions_ExecuteScalarTests; +public sealed class DbConnectionExtensions_ExecuteScalarTests_Oracle + : DbConnectionExtensions_ExecuteScalarTests; -public sealed class - DbConnectionExtensions_ExecuteScalarTests_PostgreSql : - DbConnectionExtensions_ExecuteScalarTests; +public sealed class DbConnectionExtensions_ExecuteScalarTests_PostgreSql + : DbConnectionExtensions_ExecuteScalarTests; -public sealed class - DbConnectionExtensions_ExecuteScalarTests_Sqlite : - DbConnectionExtensions_ExecuteScalarTests; +public sealed class DbConnectionExtensions_ExecuteScalarTests_Sqlite + : DbConnectionExtensions_ExecuteScalarTests; -public sealed class - DbConnectionExtensions_ExecuteScalarTests_SqlServer : - DbConnectionExtensions_ExecuteScalarTests; +public sealed class DbConnectionExtensions_ExecuteScalarTests_SqlServer + : DbConnectionExtensions_ExecuteScalarTests; -public abstract class - DbConnectionExtensions_ExecuteScalarTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_ExecuteScalarTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi - ) + public async Task ExecuteScalar_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -40,59 +33,59 @@ Boolean useAsyncApi this.DelayNextDbCommand = true; await Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - "SELECT 1", - cancellationToken: cancellationToken - ) + CallApi(useAsyncApi, this.Connection, "SELECT 1", cancellationToken: cancellationToken) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public Task ExecuteScalar_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => + public Task ExecuteScalar_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(bool useAsyncApi) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains the " + - $"value 'A' ({typeof(String)}), which could not be converted to the type {typeof(Int32)}.*" + "The first column of the first row in the result set returned by the SQL statement contains the " + + $"value 'A' ({typeof(string)}), which could not be converted to the type {typeof(int)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task ExecuteScalar_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, "GetFirstEntityId", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entity.Id); + ) + ) + .Should() + .Be(entity.Id); } [Theory] [InlineData(false)] [InlineData(true)] public async Task ExecuteScalar_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -100,90 +93,101 @@ Boolean useAsyncApi var entities = Generate.Multiple(1); InterpolatedSqlStatement statement = $""" - SELECT {Q("StringValue")} - FROM {TemporaryTable(entities)} - """; + SELECT {Q("StringValue")} + FROM {TemporaryTable(entities)} + """; var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entities[0].StringValue); + ) + ) + .Should() + .Be(entities[0].StringValue); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - ExecuteScalar_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task ExecuteScalar_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(1); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $""" - SELECT {Q("StringValue")} - FROM {TemporaryTable(entities)} - """, + SELECT {Q("StringValue")} + FROM {TemporaryTable(entities)} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entities[0].StringValue); + ) + ).Should().Be(entities[0].StringValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task ExecuteScalar_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entity.StringValue); + ) + ) + .Should() + .Be(entity.StringValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_NoResultSet_ShouldReturnDefault(Boolean useAsyncApi) + public async Task ExecuteScalar_NoResultSet_ShouldReturnDefault(bool useAsyncApi) { - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, "SELECT 1 WHERE 0 = 1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ) + ) + .Should() + .BeNull(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, "SELECT 1 WHERE 0 = 1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(0); + ) + ) + .Should() + .Be(0); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task ExecuteScalar_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -192,21 +196,22 @@ public async Task ExecuteScalar_Parameter_ShouldPassParameter(Boolean useAsyncAp ("Id", entity.Id) ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entity.StringValue); + ) + ) + .Should() + .Be(entity.StringValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi - ) + public async Task ExecuteScalar_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -216,152 +221,166 @@ Boolean useAsyncApi var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entityIds[0]); + ) + ) + .Should() + .Be(entityIds[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - ExecuteScalar_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task ExecuteScalar_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(1); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entityIds[0]); + ) + ) + .Should() + .Be(entityIds[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task ExecuteScalar_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $""" - SELECT {Q("DateTimeOffsetValue")} - FROM {Q("EntityWithDateTimeOffset")} - """, + SELECT {Q("DateTimeOffsetValue")} + FROM {Q("EntityWithDateTimeOffset")} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entity.DateTimeOffsetValue); + ) + ).Should().Be(entity.DateTimeOffsetValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthNotOne_ShouldThrow(bool useAsyncApi) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains " + - $"the value '' ({typeof(String)}), which could not be converted to the type {typeof(Char)}. " + - "See inner exception for details.*" - )) + "The first column of the first row in the result set returned by the SQL statement contains " + + $"the value '' ({typeof(string)}), which could not be converted to the type {typeof(char)}. " + + "See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains the " + - $"value 'ab' ({typeof(String)}), which could not be converted to the type {typeof(Char)}. See " + - "inner exception for details.*" - )) + "The first column of the first row in the result set returned by the SQL statement contains the " + + $"value 'ab' ({typeof(string)}), which could not be converted to the type {typeof(char)}. See " + + "inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthOne_ShouldGetFirstCharacter(Boolean useAsyncApi) + public async Task ExecuteScalar_TargetTypeIsChar_ColumnValueIsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(character); + ) + ) + .Should() + .Be(character); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInteger_ShouldConvertIntegerToEnum( - Boolean useAsyncApi - ) + public async Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInteger_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => + public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidInteger_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -370,16 +389,17 @@ public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidInteger_ShouldThr cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains the " + - $"value '999*' (System.*), which could not be converted to the type {typeof(TestEnum)}.*" + "The first column of the first row in the result set returned by the SQL statement contains the " + + $"value '999*' (System.*), which could not be converted to the type {typeof(TestEnum)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidString_ShouldThrow(Boolean useAsyncApi) => + public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidString_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -388,91 +408,105 @@ public Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsInvalidString_ShouldThro cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column of the first row in the result set returned by the SQL statement contains the " + - $"value 'NonExistent' ({typeof(String)}), which could not be converted to the type " + - $"{typeof(TestEnum)}.*" + "The first column of the first row in the result set returned by the SQL statement contains the " + + $"value 'NonExistent' ({typeof(string)}), which could not be converted to the type " + + $"{typeof(TestEnum)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsString_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task ExecuteScalar_TargetTypeIsEnum_ColumnValueIsString_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public Task ExecuteScalar_TargetTypeIsNonNullable_ColumnValueIsNull_ShouldThrow(Boolean useAsyncApi) => + public Task ExecuteScalar_TargetTypeIsNonNullable_ColumnValueIsNull_ShouldThrow(bool useAsyncApi) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "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(Int32)}.*" + "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(int)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_TargetTypeIsNullable_ColumnValueIsNull_ShouldReturnNull(Boolean useAsyncApi) => - (await CallApi( - useAsyncApi, - this.Connection, - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + public async Task ExecuteScalar_TargetTypeIsNullable_ColumnValueIsNull_ShouldReturnNull(bool useAsyncApi) => + ( + await CallApi( + useAsyncApi, + this.Connection, + "SELECT NULL", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); [Theory] [InlineData(false)] [InlineData(true)] - public async Task ExecuteScalar_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task ExecuteScalar_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("StringValue")} FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entity.StringValue); + ) + ) + .Should() + .Be(entity.StringValue); await transaction.RollbackAsync(); } - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("StringValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ) + ) + .Should() + .BeNull(); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs index 8ee17ed..2c744a1 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ExistsTests.cs @@ -2,34 +2,29 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_ExistsTests_MySql : - DbConnectionExtensions_ExistsTests; +public sealed class DbConnectionExtensions_ExistsTests_MySql + : DbConnectionExtensions_ExistsTests; -public sealed class - DbConnectionExtensions_ExistsTests_Oracle : - DbConnectionExtensions_ExistsTests; +public sealed class DbConnectionExtensions_ExistsTests_Oracle + : DbConnectionExtensions_ExistsTests; -public sealed class - DbConnectionExtensions_ExistsTests_PostgreSql : - DbConnectionExtensions_ExistsTests; +public sealed class DbConnectionExtensions_ExistsTests_PostgreSql + : DbConnectionExtensions_ExistsTests; -public sealed class - DbConnectionExtensions_ExistsTests_Sqlite : - DbConnectionExtensions_ExistsTests; +public sealed class DbConnectionExtensions_ExistsTests_Sqlite + : DbConnectionExtensions_ExistsTests; -public sealed class - DbConnectionExtensions_ExistsTests_SqlServer : - DbConnectionExtensions_ExistsTests; +public sealed class DbConnectionExtensions_ExistsTests_SqlServer + : DbConnectionExtensions_ExistsTests; -public abstract class - DbConnectionExtensions_ExistsTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_ExistsTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task Exists_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -38,103 +33,112 @@ public async Task Exists_CancellationToken_ShouldCancelOperationIfCancellationIs this.DelayNextDbCommand = true; await Invoking(() => CallApi(useAsyncApi, this.Connection, "SELECT 1", cancellationToken: cancellationToken)) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task Exists_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); this.CreateEntitiesInDb(1); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, "GetFirstEntityId", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ) + .Should() + .BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task Exists_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(1); InterpolatedSqlStatement statement = $""" - SELECT 1 - FROM {TemporaryTable(entities)} - WHERE {Q("Id")} = {Parameter(entities[0].Id)} - """; + SELECT 1 + FROM {TemporaryTable(entities)} + WHERE {Q("Id")} = {Parameter(entities[0].Id)} + """; var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ) + .Should() + .BeTrue(); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Exists_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task Exists_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(1); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $""" - SELECT 1 - FROM {TemporaryTable(entities)} - WHERE {Q("Id")} = {Parameter(entities[0].Id)} - """, + SELECT 1 + FROM {TemporaryTable(entities)} + WHERE {Q("Id")} = {Parameter(entities[0].Id)} + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ).Should().BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task Exists_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ) + .Should() + .BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task Exists_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -143,19 +147,22 @@ public async Task Exists_Parameter_ShouldPassParameter(Boolean useAsyncApi) ("Id", entity.Id) ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ) + .Should() + .BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task Exists_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -166,94 +173,111 @@ public async Task Exists_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfte var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ) + .Should() + .BeTrue(); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] public async Task Exists_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 FROM {TemporaryTable(entityIds)} WHERE {Q("Value")} = {Parameter(entityIds[0])}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ) + .Should() + .BeTrue(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_ShouldReturnBooleanIndicatingWhetherQueryReturnedAtLeastOneRow(Boolean useAsyncApi) + public async Task Exists_ShouldReturnBooleanIndicatingWhetherQueryReturnedAtLeastOneRow(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ) + .Should() + .BeTrue(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeFalse(); + ) + ) + .Should() + .BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Exists_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task Exists_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", transaction, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeTrue(); + ) + ) + .Should() + .BeTrue(); await transaction.RollbackAsync(); } - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeFalse(); + ) + ) + .Should() + .BeFalse(); } - private static Task CallApi( - Boolean useAsyncApi, + private static Task CallApi( + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -264,13 +288,7 @@ private static Task CallApi( { if (useAsyncApi) { - return connection.ExistsAsync( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ); + return connection.ExistsAsync(statement, transaction, commandTimeout, commandType, cancellationToken); } try @@ -281,7 +299,7 @@ private static Task CallApi( } catch (Exception ex) { - return Task.FromException(ex); + return Task.FromException(ex); } } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ParameterTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ParameterTests.cs index 7d32fe8..67afb9c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ParameterTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.ParameterTests.cs @@ -1,27 +1,22 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_ParameterTests_MySql : - DbConnectionExtensions_ParameterTests; +public sealed class DbConnectionExtensions_ParameterTests_MySql + : DbConnectionExtensions_ParameterTests; -public sealed class - DbConnectionExtensions_ParameterTests_Oracle : - DbConnectionExtensions_ParameterTests; +public sealed class DbConnectionExtensions_ParameterTests_Oracle + : DbConnectionExtensions_ParameterTests; -public sealed class - DbConnectionExtensions_ParameterTests_PostgreSql : - DbConnectionExtensions_ParameterTests; +public sealed class DbConnectionExtensions_ParameterTests_PostgreSql + : DbConnectionExtensions_ParameterTests; -public sealed class - DbConnectionExtensions_ParameterTests_Sqlite : - DbConnectionExtensions_ParameterTests; +public sealed class DbConnectionExtensions_ParameterTests_Sqlite + : DbConnectionExtensions_ParameterTests; -public sealed class - DbConnectionExtensions_ParameterTests_SqlServer : - DbConnectionExtensions_ParameterTests; +public sealed class DbConnectionExtensions_ParameterTests_SqlServer + : DbConnectionExtensions_ParameterTests; -public abstract class - DbConnectionExtensions_ParameterTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_ParameterTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Fact] @@ -35,12 +30,12 @@ public void Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeE // parameters have different types (integer vs. string). var enumValue1 = Generate.Single(); - this.Connection - .ExecuteScalar( + this.Connection.ExecuteScalar( $"SELECT {Parameter(enumValue1)}", cancellationToken: TestContext.Current.CancellationToken ) - .Should().Be((Int32)enumValue1); + .Should() + .Be((int)enumValue1); } [Fact] @@ -54,54 +49,54 @@ public void Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEn // parameters have different types (integer vs. string). var enumValue2 = Generate.Single(); - this.Connection - .ExecuteScalar( + this.Connection.ExecuteScalar( $"SELECT {Parameter(enumValue2)}", cancellationToken: TestContext.Current.CancellationToken ) - .Should().Be(enumValue2.ToString()); + .Should() + .Be(enumValue2.ToString()); } [Fact] public void Parameter_MultipleParameters_ShouldPassValuesAsParameters() { - const Int64 int64 = 123L; + const long int64 = 123L; var guid = Guid.NewGuid(); var dateTime = new DateTime(2025, 12, 31, 23, 59, 59); - this.Connection - .QuerySingle<(Int64, Guid, DateTime)>( + this.Connection.QuerySingle<(long, Guid, DateTime)>( $"SELECT {Parameter(int64)}, {Parameter(guid)}, {Parameter(dateTime)}", cancellationToken: TestContext.Current.CancellationToken ) - .Should().Be((int64, guid, dateTime)); + .Should() + .Be((int64, guid, dateTime)); } [Fact] public void Parameter_ShouldPassValueAsParameter() { - const Int64 int64 = 123L; - this.Connection - .ExecuteScalar( + const long int64 = 123L; + this.Connection.ExecuteScalar( $"SELECT {Parameter(int64)}", cancellationToken: TestContext.Current.CancellationToken ) - .Should().Be(int64); + .Should() + .Be(int64); var guid = Guid.NewGuid(); - this.Connection - .ExecuteScalar( + this.Connection.ExecuteScalar( $"SELECT {Parameter(guid)}", cancellationToken: TestContext.Current.CancellationToken ) - .Should().Be(guid); + .Should() + .Be(guid); var dateTime = new DateTime(2025, 12, 31, 23, 59, 59); - this.Connection - .ExecuteScalar( + this.Connection.ExecuteScalar( $"SELECT {Parameter(dateTime)}", cancellationToken: TestContext.Current.CancellationToken ) - .Should().Be(dateTime); + .Should() + .Be(dateTime); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs index de5ae7a..d7dcd4d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -2,122 +2,126 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_MySql : - DbConnectionExtensions_QueryFirstOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOfTTests_MySql + : DbConnectionExtensions_QueryFirstOfTTests; -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_Oracle : - DbConnectionExtensions_QueryFirstOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOfTTests_Oracle + : DbConnectionExtensions_QueryFirstOfTTests; -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_PostgreSql : - DbConnectionExtensions_QueryFirstOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOfTTests_PostgreSql + : DbConnectionExtensions_QueryFirstOfTTests; -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_Sqlite : - DbConnectionExtensions_QueryFirstOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOfTTests_Sqlite + : DbConnectionExtensions_QueryFirstOfTTests; -public sealed class - DbConnectionExtensions_QueryFirstOfTTests_SqlServer : - DbConnectionExtensions_QueryFirstOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOfTTests_SqlServer + : DbConnectionExtensions_QueryFirstOfTTests; -public abstract class - DbConnectionExtensions_QueryFirstOfTTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_QueryFirstOfTTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirst_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value '' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value 'ab' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QueryFirst_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(character); + ) + ) + .Should() + .Be(character); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => + public Task QueryFirst_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(bool useAsyncApi) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" + $"The first column returned by the SQL statement contains the value 'A' ({typeof(string)}), which " + + $"could not be converted to the type {typeof(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => + public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -126,16 +130,17 @@ public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_S cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" + "The first column returned by the SQL statement contains the value '999*' (System.*), which " + + $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => + public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -144,99 +149,111 @@ public Task QueryFirst_BuiltInType_EnumTargetType_ColumnContainsInvalidString_Sh cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" + "The first column returned by the SQL statement contains the value 'NonExistent' " + + $"({typeof(string)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + + "exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QueryFirst_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QueryFirst_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) => + public Task QueryFirst_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(bool useAsyncApi) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. 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(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi - ) => - (await CallApi( - useAsyncApi, - this.Connection, - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + public async Task QueryFirst_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull(bool useAsyncApi) => + ( + await CallApi( + useAsyncApi, + this.Connection, + "SELECT NULL", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QueryFirst_BuiltInType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entities[0].DateTimeOffsetValue); + ) + ) + .Should() + .Be(entities[0].DateTimeOffsetValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -252,34 +269,37 @@ await Invoking(() => cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task QueryFirst_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, "GetEntities", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -289,44 +309,49 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QueryFirst_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { @@ -340,16 +365,17 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be " + - $"converted to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be " + + $"converted to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } @@ -361,44 +387,45 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QueryFirst_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new Entity { CharValue = character }); + ) + ) + .Should() + .BeEquivalentTo(new Entity { CharValue = character }); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QueryFirst_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -407,28 +434,26 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" + "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + + $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + + $"{typeof(Entity)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_ColumnHasNoName_ShouldThrow(bool useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { - SqlServerTestDatabaseProvider => - "SELECT 1", + SqlServerTestDatabaseProvider => "SELECT 1", - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", + PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => "SELECT 1 AS \" \"", - _ => - "SELECT 1 AS ''" + _ => "SELECT 1 AS ''", }; await Invoking(() => @@ -439,210 +464,238 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" + "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + + "statement returns have a name.*" ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( - Boolean useAsyncApi - ) + public async Task QueryFirst_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirst_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( - Boolean useAsyncApi + bool useAsyncApi ) { - var entity = (await Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken + var entity = ( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", + cancellationToken: TestContext.Current.CancellationToken + ) ) - ) - .Should().NotThrowAsync()).Subject; + .Should() + .NotThrowAsync() + ).Subject; - entity - .Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); + entity.Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirst_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( - Boolean useAsyncApi + bool useAsyncApi ) { var entities = this.CreateEntitiesInDb(2); var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entitiesWithDifferentCasingProperties[0]); + ) + ) + .Should() + .BeEquivalentTo(entitiesWithDifferentCasingProperties[0]); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirst_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirst_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", + $"SELECT 1 AS {Q("Id")}, {(int)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Enum - .Should().Be(enumValue); + ) + ) + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Enum - .Should().Be(enumValue); + ) + ) + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo( + ) + ) + .Should() + .BeEquivalentTo( entity, - 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 QueryFirst_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo( + ) + ) + .Should() + .BeEquivalentTo( entity, - 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")) ); } @@ -650,82 +703,88 @@ public async Task QueryFirst_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapp [InlineData(false)] [InlineData(true)] public Task QueryFirst_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + - "(* NonExistent).*" + $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + + "(* NonExistent).*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task QueryFirst_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + bool useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task QueryFirst_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + bool useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) + public Task QueryFirst_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(bool useAsyncApi) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => CallApi( @@ -735,55 +794,60 @@ public Task QueryFirst_EntityType_NonNullableEntityProperty_ColumnContainsNull_S cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi - ) + public async Task QueryFirst_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull(bool useAsyncApi) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); + ) + ) + .Should() + .BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QueryFirst_EntityType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task QueryFirst_EntityType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); @@ -797,7 +861,8 @@ public Task QueryFirst_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAs cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); @@ -806,23 +871,26 @@ public Task QueryFirst_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAs [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task QueryFirst_Parameter_ShouldPassParameter(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -831,193 +899,204 @@ public async Task QueryFirst_Parameter_ShouldPassParameter(Boolean useAsyncApi) ("Id", entities[0].Id) ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => - Invoking(() => CallApi( + public Task QueryFirst_QueryReturnedNoRows_ShouldThrow(bool useAsyncApi) => + Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did not return any rows." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did not return any rows."); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(2); - InterpolatedSqlStatement statement = - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; + InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entityIds[0]); + ) + ) + .Should() + .Be(entityIds[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = this.CreateEntitiesInDb(2); var entityIds = entities.ConvertAll(a => a.Id); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) - """, + SELECT * + FROM {Q("Entity")} + WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ).Should().BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task QueryFirst_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(2, transaction); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); await transaction.RollbackAsync(); } - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync(); + .Should() + .ThrowAsync(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task QueryFirst_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } await Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QueryFirst_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(character)); + ) + ) + .Should() + .Be(ValueTuple.Create(character)); } [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirst_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi>( @@ -1027,18 +1106,19 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" + "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + + $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirst_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi>( @@ -1048,23 +1128,24 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirst_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi>( @@ -1074,71 +1155,77 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(enumValue)); + ) + ) + .Should() + .Be(ValueTuple.Create(enumValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QueryFirst_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(enumValue)); + ) + ) + .Should() + .Be(ValueTuple.Create(enumValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) + public Task QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(bool useAsyncApi) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } @@ -1146,102 +1233,113 @@ public Task QueryFirst_ValueTupleType_NonNullableValueTupleField_ColumnContainsN [InlineData(false)] [InlineData(true)] public async Task QueryFirst_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi + bool useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(new(null)); + ) + ) + .Should() + .Be(new(null)); } [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirst_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => - CallApi<(Int32, Int32)>( + CallApi<(int, int)>( useAsyncApi, this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. 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 1 column, but the value tuple type {typeof((int, int))} has 2 " + + "fields. Make sure that the SQL statement returns the same number of columns as the number of " + + "fields in the value tuple type.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) + public async Task QueryFirst_ValueTupleType_ShouldMaterializeBinaryData(bool useAsyncApi) { - var bytes = Generate.Single(); + var bytes = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(ValueTuple.Create(bytes)); + ) + ) + .Should() + .BeEquivalentTo(ValueTuple.Create(bytes)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QueryFirst_ValueTupleType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + ( + await CallApi<(long Id, DateTimeOffset DateTimeOffsetValue)>( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be((entities[0].Id, entities[0].DateTimeOffsetValue)); + ) + ) + .Should() + .Be((entities[0].Id, entities[0].DateTimeOffsetValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task QueryFirst_ValueTupleType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -1264,13 +1362,7 @@ private static Task CallApi( try { return Task.FromResult( - connection.QueryFirst( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ) + connection.QueryFirst(statement, transaction, commandTimeout, commandType, cancellationToken) ); } catch (Exception ex) diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index 073e68f..2029f45 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -2,127 +2,129 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_MySql : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultOfTTests_MySql + : DbConnectionExtensions_QueryFirstOrDefaultOfTTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_Oracle : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultOfTTests_Oracle + : DbConnectionExtensions_QueryFirstOrDefaultOfTTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_PostgreSql : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultOfTTests_PostgreSql + : DbConnectionExtensions_QueryFirstOrDefaultOfTTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_Sqlite : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultOfTTests_Sqlite + : DbConnectionExtensions_QueryFirstOrDefaultOfTTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests_SqlServer : - DbConnectionExtensions_QueryFirstOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultOfTTests_SqlServer + : DbConnectionExtensions_QueryFirstOrDefaultOfTTests; -public abstract class - DbConnectionExtensions_QueryFirstOrDefaultOfTTests : IntegrationTestsBase< - TTestDatabaseProvider> +public abstract class DbConnectionExtensions_QueryFirstOrDefaultOfTTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value '' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value 'ab' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(character); + ) + ) + .Should() + .Be(character); } [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" + $"The first column returned by the SQL statement contains the value 'A' ({typeof(string)}), which " + + $"could not be converted to the type {typeof(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi( @@ -132,17 +134,18 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" + "The first column returned by the SQL statement contains the value '999*' (System.*), which " + + $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi( @@ -152,102 +155,116 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" + "The first column returned by the SQL statement contains the value 'NonExistent' " + + $"({typeof(string)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + + "exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. 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(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi + bool useAsyncApi ) => - (await CallApi( - useAsyncApi, - this.Connection, - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ( + await CallApi( + useAsyncApi, + this.Connection, + "SELECT NULL", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_BuiltInType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entities[0].DateTimeOffsetValue); + ) + ) + .Should() + .Be(entities[0].DateTimeOffsetValue); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -264,34 +281,39 @@ await Invoking(() => cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, "GetEntities", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -301,46 +323,49 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { @@ -354,16 +379,17 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be " + - $"converted to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be " + + $"converted to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } @@ -375,43 +401,46 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new Entity { CharValue = character }); + ) + ) + .Should() + .BeEquivalentTo(new Entity { CharValue = character }); } [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi( @@ -421,28 +450,26 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" + "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + + $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + + $"{typeof(Entity)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_EntityType_ColumnHasNoName_ShouldThrow(bool useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { - SqlServerTestDatabaseProvider => - "SELECT 1", + SqlServerTestDatabaseProvider => "SELECT 1", - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", + PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => "SELECT 1 AS \" \"", - _ => - "SELECT 1 AS ''" + _ => "SELECT 1 AS ''", }; await Invoking(() => @@ -453,10 +480,11 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" + "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + + "statement returns have a name.*" ); } @@ -464,205 +492,230 @@ await Invoking(() => [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( - Boolean useAsyncApi + bool useAsyncApi ) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor( - Boolean useAsyncApi + bool useAsyncApi ) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( + bool useAsyncApi + ) { - var entity = (await Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken + var entity = ( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", + cancellationToken: TestContext.Current.CancellationToken + ) ) - ) - .Should().NotThrowAsync()).Subject; + .Should() + .NotThrowAsync() + ).Subject; - entity - .Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); + entity.Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( + bool useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entitiesWithDifferentCasingProperties[0]); + ) + ) + .Should() + .BeEquivalentTo(entitiesWithDifferentCasingProperties[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi - ) => - await Invoking(() => CallApi( + public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( + bool useAsyncApi + ) => + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi - ) => - await Invoking(() => CallApi( + public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( + bool useAsyncApi + ) => + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", + $"SELECT 1 AS {Q("Id")}, {(int)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken - ))! - .Enum - .Should().Be(enumValue); + ) + )! + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken - ))! - .Enum - .Should().Be(enumValue); + ) + )! + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo( + ) + ) + .Should() + .BeEquivalentTo( entity, - 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 QueryFirstOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo( + ) + ) + .Should() + .BeEquivalentTo( entity, - 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")) ); } @@ -670,86 +723,90 @@ public async Task QueryFirstOrDefault_EntityType_Mapping_FluentApi_ShouldUseFlue [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + - "(* NonExistent).*" + $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + + "(* NonExistent).*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + bool useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + bool useAsyncApi + ) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => CallApi( @@ -759,10 +816,11 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -770,44 +828,50 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi + bool useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); + ) + ) + .Should() + .BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_EntityType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirstOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task QueryFirstOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); @@ -821,7 +885,8 @@ public Task QueryFirstOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Bool cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); @@ -830,23 +895,26 @@ public Task QueryFirstOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Bool [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entities[0].Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -855,208 +923,230 @@ public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(Boolean useA ("Id", entities[0].Id) ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_QueryReturnedNoRows_ShouldReturnDefault(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_QueryReturnedNoRows_ShouldReturnDefault(bool useAsyncApi) { - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("Id")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(0); + ) + ) + .Should() + .Be(0); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ) + ) + .Should() + .BeNull(); - (await CallApi<(Int64, String)>( + ( + await CallApi<(long, string)>( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(default); + ) + ) + .Should() + .Be(default); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(2); - InterpolatedSqlStatement statement = - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; + InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entityIds[0]); + ) + ) + .Should() + .Be(entityIds[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = this.CreateEntitiesInDb(2); var entityIds = entities.ConvertAll(a => a.Id); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) - """, + SELECT * + FROM {Q("Entity")} + WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ).Should().BeEquivalentTo(entities[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(2, transaction); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entities[0]); + ) + ) + .Should() + .BeEquivalentTo(entities[0]); await transaction.RollbackAsync(); } - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ) + ) + .Should() + .BeNull(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } await Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(character)); + ) + ) + .Should() + .Be(ValueTuple.Create(character)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task - QueryFirstOrDefault_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QueryFirstOrDefault_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( + bool useAsyncApi + ) => Invoking(() => CallApi>( useAsyncApi, @@ -1065,20 +1155,20 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" + "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + + $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task - QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( + bool useAsyncApi + ) => Invoking(() => CallApi>( useAsyncApi, @@ -1087,23 +1177,24 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi>( @@ -1113,182 +1204,195 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum( - Boolean useAsyncApi + bool useAsyncApi ) { var enumValue = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(enumValue)); + ) + ) + .Should() + .Be(ValueTuple.Create(enumValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(enumValue)); + ) + ) + .Should() + .Be(ValueTuple.Create(enumValue)); } [Theory] [InlineData(false)] [InlineData(true)] public Task QueryFirstOrDefault_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( + bool useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(new(null)); + ) + ) + .Should() + .Be(new(null)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task - QueryFirstOrDefault_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QueryFirstOrDefault_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( + bool useAsyncApi + ) => Invoking(() => - CallApi<(Int32, Int32)>( + CallApi<(int, int)>( useAsyncApi, this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. 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 1 column, but the value tuple type {typeof((int, int))} has 2 " + + "fields. Make sure that the SQL statement returns the same number of columns as the number of " + + "fields in the value tuple type.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_ValueTupleType_ShouldMaterializeBinaryData(bool useAsyncApi) { - var bytes = Generate.Single(); + var bytes = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(ValueTuple.Create(bytes)); + ) + ) + .Should() + .BeEquivalentTo(ValueTuple.Create(bytes)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_ValueTupleType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(2); - (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + ( + await CallApi<(long Id, DateTimeOffset DateTimeOffsetValue)>( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be((entities[0].Id, entities[0].DateTimeOffsetValue)); + ) + ) + .Should() + .Be((entities[0].Id, entities[0].DateTimeOffsetValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirstOrDefault_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task QueryFirstOrDefault_ValueTupleType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs index a723cf6..720523d 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs @@ -4,35 +4,30 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultTests_MySql : - DbConnectionExtensions_QueryFirstOrDefaultTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultTests_MySql + : DbConnectionExtensions_QueryFirstOrDefaultTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultTests_Oracle : - DbConnectionExtensions_QueryFirstOrDefaultTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultTests_Oracle + : DbConnectionExtensions_QueryFirstOrDefaultTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultTests_PostgreSql : - DbConnectionExtensions_QueryFirstOrDefaultTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultTests_PostgreSql + : DbConnectionExtensions_QueryFirstOrDefaultTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultTests_Sqlite : - DbConnectionExtensions_QueryFirstOrDefaultTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultTests_Sqlite + : DbConnectionExtensions_QueryFirstOrDefaultTests; -public sealed class - DbConnectionExtensions_QueryFirstOrDefaultTests_SqlServer : - DbConnectionExtensions_QueryFirstOrDefaultTests; +public sealed class DbConnectionExtensions_QueryFirstOrDefaultTests_SqlServer + : DbConnectionExtensions_QueryFirstOrDefaultTests; -public abstract class - DbConnectionExtensions_QueryFirstOrDefaultTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_QueryFirstOrDefaultTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -49,14 +44,15 @@ await Invoking(() => cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); @@ -77,7 +73,7 @@ public async Task QueryFirstOrDefault_CommandType_ShouldUseCommandType(Boolean u [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -97,17 +93,15 @@ Boolean useAsyncApi EntityAssertions.AssertDataRowMatchesEntity(dataRow!, entities[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -126,7 +120,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -143,7 +137,7 @@ public async Task QueryFirstOrDefault_InterpolatedParameter_ShouldPassInterpolat [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -162,24 +156,27 @@ public async Task QueryFirstOrDefault_Parameter_ShouldPassParameter(Boolean useA EntityAssertions.AssertDataRowMatchesEntity(dataRow!, entities[0]); } - [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_QueryReturnedNoRows_ShouldReturnNull(Boolean useAsyncApi) => - ((Object?)await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + public async Task QueryFirstOrDefault_QueryReturnedNoRows_ShouldReturnNull(bool useAsyncApi) => + ( + (object?) + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); [Theory] [InlineData(false)] [InlineData(true)] public async Task QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -197,23 +194,19 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - dataRow - .Should().NotBeNull(); + dataRow.Should().NotBeNull(); - dataRow["Id"] - .Should().Be(entityIds[0]); + dataRow["Id"].Should().Be(entityIds[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QueryFirstOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -226,14 +219,13 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - ValueConverter.ConvertValueToType(dataRow!["Id"]) - .Should().Be(entityIds[0]); + ValueConverter.ConvertValueToType(dataRow!["Id"]).Should().Be(entityIds[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_ShouldReturnDataRowForFirstRow(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_ShouldReturnDataRowForFirstRow(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -250,7 +242,7 @@ public async Task QueryFirstOrDefault_ShouldReturnDataRowForFirstRow(Boolean use [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { @@ -269,17 +261,21 @@ public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(Boolean u await transaction.RollbackAsync(); } - ((Object?)await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ( + (object?) + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -302,13 +298,7 @@ public async Task QueryFirstOrDefault_Transaction_ShouldUseTransaction(Boolean u try { return Task.FromResult( - connection.QueryFirstOrDefault( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ) + connection.QueryFirstOrDefault(statement, transaction, commandTimeout, commandType, cancellationToken) ); } catch (Exception ex) diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs index 7430727..86f5fa2 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryFirstTests.cs @@ -4,34 +4,29 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QueryFirstTests_MySql : - DbConnectionExtensions_QueryFirstTests; +public sealed class DbConnectionExtensions_QueryFirstTests_MySql + : DbConnectionExtensions_QueryFirstTests; -public sealed class - DbConnectionExtensions_QueryFirstTests_Oracle : - DbConnectionExtensions_QueryFirstTests; +public sealed class DbConnectionExtensions_QueryFirstTests_Oracle + : DbConnectionExtensions_QueryFirstTests; -public sealed class - DbConnectionExtensions_QueryFirstTests_PostgreSql : - DbConnectionExtensions_QueryFirstTests; +public sealed class DbConnectionExtensions_QueryFirstTests_PostgreSql + : DbConnectionExtensions_QueryFirstTests; -public sealed class - DbConnectionExtensions_QueryFirstTests_Sqlite : - DbConnectionExtensions_QueryFirstTests; +public sealed class DbConnectionExtensions_QueryFirstTests_Sqlite + : DbConnectionExtensions_QueryFirstTests; -public sealed class - DbConnectionExtensions_QueryFirstTests_SqlServer : - DbConnectionExtensions_QueryFirstTests; +public sealed class DbConnectionExtensions_QueryFirstTests_SqlServer + : DbConnectionExtensions_QueryFirstTests; -public abstract class - DbConnectionExtensions_QueryFirstTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_QueryFirstTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task QueryFirst_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -47,14 +42,15 @@ await Invoking(() => cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task QueryFirst_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); @@ -74,9 +70,7 @@ public async Task QueryFirst_CommandType_ShouldUseCommandType(Boolean useAsyncAp [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi - ) + public async Task QueryFirst_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -95,17 +89,15 @@ Boolean useAsyncApi EntityAssertions.AssertDataRowMatchesEntity(dataRow, entities[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QueryFirst_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -124,7 +116,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -141,7 +133,7 @@ public async Task QueryFirst_InterpolatedParameter_ShouldPassInterpolatedParamet [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task QueryFirst_Parameter_ShouldPassParameter(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -160,27 +152,26 @@ public async Task QueryFirst_Parameter_ShouldPassParameter(Boolean useAsyncApi) EntityAssertions.AssertDataRowMatchesEntity(dataRow, entities[0]); } - [Theory] [InlineData(false)] [InlineData(true)] - public Task QueryFirst_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => - Invoking(() => CallApi( + public Task QueryFirst_QueryReturnedNoRows_ShouldThrow(bool useAsyncApi) => + Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did not return any rows." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did not return any rows."); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -197,23 +188,19 @@ public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldDropTemporaryTable cancellationToken: TestContext.Current.CancellationToken ); - dataRow - .Should().NotBeNull(); + dataRow.Should().NotBeNull(); - dataRow["Id"] - .Should().Be(entityIds[0]); + dataRow["Id"].Should().Be(entityIds[0]); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QueryFirst_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QueryFirst_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -226,14 +213,13 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - ValueConverter.ConvertValueToType(dataRow["Id"]) - .Should().Be(entityIds[0]); + ValueConverter.ConvertValueToType(dataRow["Id"]).Should().Be(entityIds[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_ShouldReturnDataRowForFirstRow(Boolean useAsyncApi) + public async Task QueryFirst_ShouldReturnDataRowForFirstRow(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(2); @@ -250,7 +236,7 @@ public async Task QueryFirst_ShouldReturnDataRowForFirstRow(Boolean useAsyncApi) [Theory] [InlineData(false)] [InlineData(true)] - public async Task QueryFirst_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task QueryFirst_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { @@ -269,21 +255,21 @@ public async Task QueryFirst_Transaction_ShouldUseTransaction(Boolean useAsyncAp await transaction.RollbackAsync(); } - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did not return any rows." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did not return any rows."); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -294,25 +280,13 @@ private static Task CallApi( { if (useAsyncApi) { - return connection.QueryFirstAsync( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ); + return connection.QueryFirstAsync(statement, transaction, commandTimeout, commandType, cancellationToken); } try { return Task.FromResult( - connection.QueryFirst( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ) + connection.QueryFirst(statement, transaction, commandTimeout, commandType, cancellationToken) ); } catch (Exception ex) diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs index 7ad6a72..1ff9edc 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryOfTTests.cs @@ -2,78 +2,83 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QueryOfTTests_MySql : - DbConnectionExtensions_QueryOfTTests; +public sealed class DbConnectionExtensions_QueryOfTTests_MySql + : DbConnectionExtensions_QueryOfTTests; -public sealed class - DbConnectionExtensions_QueryOfTTests_Oracle : - DbConnectionExtensions_QueryOfTTests; +public sealed class DbConnectionExtensions_QueryOfTTests_Oracle + : DbConnectionExtensions_QueryOfTTests; -public sealed class - DbConnectionExtensions_QueryOfTTests_PostgreSql : - DbConnectionExtensions_QueryOfTTests; +public sealed class DbConnectionExtensions_QueryOfTTests_PostgreSql + : DbConnectionExtensions_QueryOfTTests; -public sealed class - DbConnectionExtensions_QueryOfTTests_Sqlite : - DbConnectionExtensions_QueryOfTTests; +public sealed class DbConnectionExtensions_QueryOfTTests_Sqlite + : DbConnectionExtensions_QueryOfTTests; -public sealed class - DbConnectionExtensions_QueryOfTTests_SqlServer : - DbConnectionExtensions_QueryOfTTests; +public sealed class DbConnectionExtensions_QueryOfTTests_SqlServer + : DbConnectionExtensions_QueryOfTTests; -public abstract class - DbConnectionExtensions_QueryOfTTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_QueryOfTTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] public async Task Query_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - (await Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - "SELECT ''", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + ( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + "SELECT ''", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value '' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } - (await Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - "SELECT 'ab'", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + ( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + "SELECT 'ab'", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value 'ab' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } @@ -81,160 +86,192 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task Query_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi + bool useAsyncApi ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([character]); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT '{character}'", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([character]); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => + public Task Query_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(bool useAsyncApi) => Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - "SELECT 'A'", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + CallApi( + useAsyncApi, + this.Connection, + "SELECT 'A'", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" + $"The first column returned by the SQL statement contains the value 'A' ({typeof(string)}), which " + + $"could not be converted to the type {typeof(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => + public Task Query_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( - useAsyncApi, - this.Connection, - "SELECT 999", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + "SELECT 999", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" + "The first column returned by the SQL statement contains the value '999*' (System.*), which " + + $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => + public Task Query_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( - useAsyncApi, - this.Connection, - "SELECT 'NonExistent'", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + "SELECT 'NonExistent'", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" + "The first column returned by the SQL statement contains the value 'NonExistent' " + + $"({typeof(string)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + + "exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task Query_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([enumValue]); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT {(int)enumValue}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([enumValue]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task Query_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT '{enumValue.ToString()}'", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([enumValue]); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT '{enumValue.ToString()}'", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([enumValue]); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) => + public Task Query_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(bool useAsyncApi) => Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + CallApi( + useAsyncApi, + this.Connection, + "SELECT NULL", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. 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(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull(Boolean useAsyncApi) => - (await CallApi( - useAsyncApi, - this.Connection, - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask()) - .Should().BeEquivalentTo(new Int32?[] { null }); + public async Task Query_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull(bool useAsyncApi) => + ( + await CallApi( + useAsyncApi, + this.Connection, + "SELECT NULL", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() + ) + .Should() + .BeEquivalentTo(new int?[] { null }); [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task Query_BuiltInType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities.Select(e => e.DateTimeOffsetValue)); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities.Select(e => e.DateTimeOffsetValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -244,40 +281,48 @@ public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsR await Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ).ToListAsync(cancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: cancellationToken + ) + .ToListAsync(cancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task Query_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - "GetEntities", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + "GetEntities", + commandType: CommandType.StoredProcedure, + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished(Boolean useAsyncApi) + public async Task Query_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -288,57 +333,59 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; var asyncEnumerator = CallApi( - useAsyncApi, - this.Connection, - statement, - cancellationToken: TestContext.Current.CancellationToken - ).GetAsyncEnumerator(); + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + ) + .GetAsyncEnumerator(); - (await asyncEnumerator.MoveNextAsync()) - .Should().BeTrue(); + (await asyncEnumerator.MoveNextAsync()).Should().BeTrue(); if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeTrue(); } - (await asyncEnumerator.MoveNextAsync()) - .Should().BeTrue(); + (await asyncEnumerator.MoveNextAsync()).Should().BeTrue(); - (await asyncEnumerator.MoveNextAsync()) - .Should().BeFalse(); + (await asyncEnumerator.MoveNextAsync()).Should().BeFalse(); await asyncEnumerator.DisposeAsync(); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable(Boolean useAsyncApi) + public async Task Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {TemporaryTable(entities)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow(Boolean useAsyncApi) + public async Task Query_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { @@ -346,449 +393,520 @@ public async Task await Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT '' AS {Q("CharValue")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT '' AS {Q("CharValue")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be " + - $"converted to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be " + + $"converted to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } await Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT 'ab' AS {Q("CharValue")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT 'ab' AS {Q("CharValue")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task Query_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT '{character}' AS {Q("CharValue")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([new Entity { CharValue = character }]); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT '{character}' AS {Q("CharValue")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([new Entity { CharValue = character }]); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow(Boolean useAsyncApi) => + public Task Query_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT 123 AS {Q("TimeSpanValue")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT 123 AS {Q("TimeSpanValue")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" + "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + + $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + + $"{typeof(Entity)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) + public async Task Query_EntityType_ColumnHasNoName_ShouldThrow(bool useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { - SqlServerTestDatabaseProvider => - "SELECT 1", + SqlServerTestDatabaseProvider => "SELECT 1", - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", + PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => "SELECT 1 AS \" \"", - _ => - "SELECT 1 AS ''" + _ => "SELECT 1 AS ''", }; await Invoking(() => CallApi( - useAsyncApi, - this.Connection, - statement, - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" + "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + + "statement returns have a name.*" ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor(Boolean useAsyncApi) + public async Task Query_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(Boolean useAsyncApi) + public async Task Query_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] public async Task Query_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( - Boolean useAsyncApi + bool useAsyncApi ) { - var entities = (await Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() - ) - .Should().NotThrowAsync()).Subject; + var entities = ( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() + ) + .Should() + .NotThrowAsync() + ).Subject; - entities - .Should().BeEquivalentTo([new Entity { Id = 1, Int32Value = 2 }]); + entities.Should().BeEquivalentTo([new Entity { Id = 1, Int32Value = 2 }]); } [Theory] [InlineData(false)] [InlineData(true)] public async Task Query_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( - Boolean useAsyncApi + bool useAsyncApi ) { var entities = this.CreateEntitiesInDb(); var entitiesWithDifferentCasingProperties = Generate.MapTo(entities); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entitiesWithDifferentCasingProperties); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entitiesWithDifferentCasingProperties); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi - ) => - await Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + public async Task Query_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow(bool useAsyncApi) => + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi - ) => - await Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + public async Task Query_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow(bool useAsyncApi) => + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task Query_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ).FirstAsync()) - .Enum - .Should().Be(enumValue); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, {(int)enumValue} AS {Q("Enum")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .FirstAsync() + ) + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task Query_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", - cancellationToken: TestContext.Current.CancellationToken - ).FirstAsync()) - .Enum - .Should().Be(enumValue); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .FirstAsync() + ) + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task Query_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo( + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .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 Query_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task Query_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo( + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .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 Query_EntityType_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task Query_EntityType_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("MappingTestEntity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("MappingTestEntity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow(Boolean useAsyncApi) => + public Task Query_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") - .ToListAsync(TestContext.Current.CancellationToken).AsTask() + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + - "(* NonExistent).*" + $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + + "(* NonExistent).*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task Query_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + bool useAsyncApi + ) { var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task Query_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + bool useAsyncApi + ) { var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) + public Task Query_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(bool useAsyncApi) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull(Boolean useAsyncApi) + public async Task Query_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull(bool useAsyncApi) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([new Entity { Id = 1, NullableBooleanValue = null }]); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([new Entity { Id = 1, NullableBooleanValue = null }]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task Query_EntityType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task Query_EntityType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); @@ -796,13 +914,16 @@ public Task Query_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncAp return Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT {literal} AS {Q("Value")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); @@ -811,23 +932,27 @@ public Task Query_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncAp [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task Query_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([entity]); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([entity]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task Query_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -836,117 +961,128 @@ public async Task Query_Parameter_ShouldPassParameter(Boolean useAsyncApi) ("Id", entity.Id) ); - (await CallApi( - useAsyncApi, - this.Connection, - statement, - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([entity]); + ( + await CallApi( + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([entity]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished(Boolean useAsyncApi) + public async Task Query_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(2); - InterpolatedSqlStatement statement = - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; + InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}"; var temporaryTableName = statement.TemporaryTables[0].Name; var asyncEnumerator = CallApi( - useAsyncApi, - this.Connection, - statement, - cancellationToken: TestContext.Current.CancellationToken - ).GetAsyncEnumerator(); + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + ) + .GetAsyncEnumerator(); - (await asyncEnumerator.MoveNextAsync()) - .Should().BeTrue(); + (await asyncEnumerator.MoveNextAsync()).Should().BeTrue(); if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeTrue(); } - (await asyncEnumerator.MoveNextAsync()) - .Should().BeTrue(); + (await asyncEnumerator.MoveNextAsync()).Should().BeTrue(); - (await asyncEnumerator.MoveNextAsync()) - .Should().BeFalse(); + (await asyncEnumerator.MoveNextAsync()).Should().BeFalse(); await asyncEnumerator.DisposeAsync(); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable(Boolean useAsyncApi) + public async Task Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = this.CreateEntitiesInDb(5); var entityIds = entities.Take(2).Select(a => a.Id).ToList(); - (await CallApi( - useAsyncApi, - this.Connection, - $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) - """, - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities.Take(2)); + ( + await CallApi( + useAsyncApi, + this.Connection, + $""" + SELECT * + FROM {Q("Entity")} + WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}) + """, + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ).Should().BeEquivalentTo(entities.Take(2)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task Query_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(null, transaction); - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + transaction, + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); await transaction.RollbackAsync(); } - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEmpty(); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEmpty(); } [Theory] [InlineData(false)] [InlineData(true)] public async Task Query_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) @@ -954,293 +1090,332 @@ Boolean useAsyncApi // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi>( - useAsyncApi, - this.Connection, - $"SELECT '' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + CallApi>( + useAsyncApi, + this.Connection, + $"SELECT '' AS {Q("Value")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } await Invoking(() => - CallApi>( - useAsyncApi, - this.Connection, - $"SELECT 'ab' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + CallApi>( + useAsyncApi, + this.Connection, + $"SELECT 'ab' AS {Q("Value")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - Query_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task Query_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi>( - useAsyncApi, - this.Connection, - $"SELECT '{character}'", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([ValueTuple.Create(character)]); + ( + await CallApi>( + useAsyncApi, + this.Connection, + $"SELECT '{character}'", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([ValueTuple.Create(character)]); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( - Boolean useAsyncApi - ) => + public Task Query_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi>( - useAsyncApi, - this.Connection, - $"SELECT 123 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT 123 AS {Q("Value")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" + "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + + $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi - ) => + public Task Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi>( - useAsyncApi, - this.Connection, - $"SELECT 999 AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT 999 AS {Q("Value")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => + public Task Query_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi>( - useAsyncApi, - this.Connection, - $"SELECT 'NonExistent' AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT 'NonExistent' AS {Q("Value")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task Query_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi>( - useAsyncApi, - this.Connection, - $"SELECT {(Int32)enumValue}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([ValueTuple.Create(enumValue)]); + ( + await CallApi>( + useAsyncApi, + this.Connection, + $"SELECT {(int)enumValue}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([ValueTuple.Create(enumValue)]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task Query_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi>( - useAsyncApi, - this.Connection, - $"SELECT '{enumValue}'", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([ValueTuple.Create(enumValue)]); + ( + await CallApi>( + useAsyncApi, + this.Connection, + $"SELECT '{enumValue}'", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([ValueTuple.Create(enumValue)]); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) + public Task Query_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(bool useAsyncApi) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => - CallApi>( - useAsyncApi, - this.Connection, - $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + CallApi>( + useAsyncApi, + this.Connection, + $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi - ) + public async Task Query_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull(bool useAsyncApi) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( - useAsyncApi, - this.Connection, - $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([new ValueTuple(null)]); + ( + await CallApi>( + useAsyncApi, + this.Connection, + $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([new ValueTuple(null)]); } [Theory] [InlineData(false)] [InlineData(true)] public Task Query_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => - CallApi<(Int32, Int32)>( - useAsyncApi, - this.Connection, - "SELECT 1", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + CallApi<(int, int)>( + useAsyncApi, + this.Connection, + "SELECT 1", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. 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 1 column, but the value tuple type {typeof((int, int))} has 2 " + + "fields. Make sure that the SQL statement returns the same number of columns as the number of " + + "fields in the value tuple type.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) + public async Task Query_ValueTupleType_ShouldMaterializeBinaryData(bool useAsyncApi) { - var bytes = Generate.Single(); + var bytes = Generate.Single(); - (await CallApi>( - useAsyncApi, - this.Connection, - $"SELECT {Parameter(bytes)} AS BinaryData", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo([ValueTuple.Create(bytes)]); + ( + await CallApi>( + useAsyncApi, + this.Connection, + $"SELECT {Parameter(bytes)} AS BinaryData", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo([ValueTuple.Create(bytes)]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task Query_ValueTupleType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entities = this.CreateEntitiesInDb(); - (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( - useAsyncApi, - this.Connection, - $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities.Select(e => (e.Id, e.DateTimeOffsetValue))); + ( + await CallApi<(long Id, DateTimeOffset DateTimeOffsetValue)>( + useAsyncApi, + this.Connection, + $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities.Select(e => (e.Id, e.DateTimeOffsetValue))); } [Theory] [InlineData(false)] [InlineData(true)] - public Task Query_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task Query_ValueTupleType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - CallApi>( - useAsyncApi, - this.Connection, - $"SELECT {literal} AS {Q("Value")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken).AsTask() + CallApi>( + useAsyncApi, + this.Connection, + $"SELECT {literal} AS {Q("Value")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } private static IAsyncEnumerable CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -1251,21 +1426,11 @@ private static IAsyncEnumerable CallApi( { if (useAsyncApi) { - return connection.QueryAsync( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ); + return connection.QueryAsync(statement, transaction, commandTimeout, commandType, cancellationToken); } - return connection.Query( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ).ToAsyncEnumerable(); + return connection + .Query(statement, transaction, commandTimeout, commandType, cancellationToken) + .ToAsyncEnumerable(); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs index 9ed0dd6..c000ea3 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -2,122 +2,126 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_MySql : - DbConnectionExtensions_QuerySingleOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOfTTests_MySql + : DbConnectionExtensions_QuerySingleOfTTests; -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_Oracle : - DbConnectionExtensions_QuerySingleOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOfTTests_Oracle + : DbConnectionExtensions_QuerySingleOfTTests; -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_PostgreSql : - DbConnectionExtensions_QuerySingleOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOfTTests_PostgreSql + : DbConnectionExtensions_QuerySingleOfTTests; -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_Sqlite : - DbConnectionExtensions_QuerySingleOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOfTTests_Sqlite + : DbConnectionExtensions_QuerySingleOfTTests; -public sealed class - DbConnectionExtensions_QuerySingleOfTTests_SqlServer : - DbConnectionExtensions_QuerySingleOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOfTTests_SqlServer + : DbConnectionExtensions_QuerySingleOfTTests; -public abstract class - DbConnectionExtensions_QuerySingleOfTTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_QuerySingleOfTTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingle_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value '' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value 'ab' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QuerySingle_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(character); + ) + ) + .Should() + .Be(character); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(Boolean useAsyncApi) => + public Task QuerySingle_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow(bool useAsyncApi) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" + $"The first column returned by the SQL statement contains the value 'A' ({typeof(string)}), which " + + $"could not be converted to the type {typeof(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(Boolean useAsyncApi) => + public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -126,16 +130,17 @@ public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" + "The first column returned by the SQL statement contains the value '999*' (System.*), which " + + $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(Boolean useAsyncApi) => + public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow(bool useAsyncApi) => Invoking(() => CallApi( useAsyncApi, @@ -144,99 +149,113 @@ public Task QuerySingle_BuiltInType_EnumTargetType_ColumnContainsInvalidString_S cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" + "The first column returned by the SQL statement contains the value 'NonExistent' " + + $"({typeof(string)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + + "exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QuerySingle_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QuerySingle_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) => + public Task QuerySingle_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow(bool useAsyncApi) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. 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(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingle_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi + bool useAsyncApi ) => - (await CallApi( - useAsyncApi, - this.Connection, - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ( + await CallApi( + useAsyncApi, + this.Connection, + "SELECT NULL", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QuerySingle_BuiltInType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entity.DateTimeOffsetValue); + ) + ) + .Should() + .Be(entity.DateTimeOffsetValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -252,34 +271,37 @@ await Invoking(() => cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task QuerySingle_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, "GetFirstEntity", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QuerySingle_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -289,44 +311,49 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QuerySingle_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {TemporaryTable([entity])}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { @@ -340,16 +367,17 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be " + - $"converted to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be " + + $"converted to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } @@ -361,43 +389,46 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QuerySingle_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new Entity { CharValue = character }); + ) + ) + .Should() + .BeEquivalentTo(new Entity { CharValue = character }); } [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingle_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi( @@ -407,28 +438,26 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" + "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + + $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + + $"{typeof(Entity)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_ColumnHasNoName_ShouldThrow(bool useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { - SqlServerTestDatabaseProvider => - "SELECT 1", + SqlServerTestDatabaseProvider => "SELECT 1", - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", + PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => "SELECT 1 AS \" \"", - _ => - "SELECT 1 AS ''" + _ => "SELECT 1 AS ''", }; await Invoking(() => @@ -439,211 +468,238 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" + "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + + "statement returns have a name.*" ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( - Boolean useAsyncApi - ) + public async Task QuerySingle_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingle_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( - Boolean useAsyncApi + bool useAsyncApi ) { - var entity = (await Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken + var entity = ( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", + cancellationToken: TestContext.Current.CancellationToken + ) ) - ) - .Should().NotThrowAsync()).Subject; + .Should() + .NotThrowAsync() + ).Subject; - entity - .Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); + entity.Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( - Boolean useAsyncApi - ) + public async Task QuerySingle_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( + bool useAsyncApi + ) { var entity = this.CreateEntityInDb(); var entityWithDifferentCasingProperties = Generate.MapTo(entity); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithDifferentCasingProperties); + ) + ) + .Should() + .BeEquivalentTo(entityWithDifferentCasingProperties); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingle_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingle_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", + $"SELECT 1 AS {Q("Id")}, {(int)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Enum - .Should().Be(enumValue); + ) + ) + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Enum - .Should().Be(enumValue); + ) + ) + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo( + ) + ) + .Should() + .BeEquivalentTo( entity, - 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 QuerySingle_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo( + ) + ) + .Should() + .BeEquivalentTo( entity, - 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")) ); } @@ -651,82 +707,88 @@ public async Task QuerySingle_EntityType_Mapping_FluentApi_ShouldUseFluentApiMap [InlineData(false)] [InlineData(true)] public Task QuerySingle_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + - "(* NonExistent).*" + $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + + "(* NonExistent).*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task QuerySingle_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + bool useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task QuerySingle_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + bool useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(Boolean useAsyncApi) + public Task QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow(bool useAsyncApi) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => CallApi( @@ -736,10 +798,11 @@ public Task QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -747,44 +810,50 @@ public Task QuerySingle_EntityType_NonNullableEntityProperty_ColumnContainsNull_ [InlineData(false)] [InlineData(true)] public async Task QuerySingle_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi + bool useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); + ) + ) + .Should() + .BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QuerySingle_EntityType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task QuerySingle_EntityType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); @@ -798,7 +867,8 @@ public Task QuerySingle_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useA cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); @@ -807,23 +877,26 @@ public Task QuerySingle_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useA [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task QuerySingle_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -832,213 +905,224 @@ public async Task QuerySingle_Parameter_ShouldPassParameter(Boolean useAsyncApi) ("Id", entity.Id) ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_QueryReturnedMoreThanOneRow_ShouldThrow(Boolean useAsyncApi) + public async Task QuerySingle_QueryReturnedMoreThanOneRow_ShouldThrow(bool useAsyncApi) { this.CreateEntitiesInDb(2); - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did return more than one row." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did return more than one row."); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => - Invoking(() => CallApi( + public Task QuerySingle_QueryReturnedNoRows_ShouldThrow(bool useAsyncApi) => + Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did not return any rows." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did not return any rows."); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityId = Generate.Id(); - InterpolatedSqlStatement statement = - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}"; + InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}"; var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entityId); + ) + ) + .Should() + .Be(entityId); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = this.CreateEntityInDb(); var entityId = entity.Id; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable([entityId])}) - """, + SELECT * + FROM {Q("Entity")} + WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable([entityId])}) + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ).Should().BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task QuerySingle_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); await transaction.RollbackAsync(); } - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync(); + .Should() + .ThrowAsync(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task QuerySingle_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } await Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QuerySingle_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(character)); + ) + ) + .Should() + .Be(ValueTuple.Create(character)); } [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingle_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi>( @@ -1048,18 +1132,19 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" + "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + + $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingle_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi>( @@ -1069,23 +1154,24 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingle_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi>( @@ -1095,73 +1181,77 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(enumValue)); + ) + ) + .Should() + .Be(ValueTuple.Create(enumValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QuerySingle_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(enumValue)); + ) + ) + .Should() + .Be(ValueTuple.Create(enumValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi - ) + public Task QuerySingle_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow(bool useAsyncApi) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } @@ -1169,102 +1259,113 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task QuerySingle_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi + bool useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(new(null)); + ) + ) + .Should() + .Be(new(null)); } [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingle_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => - CallApi<(Int32, Int32)>( + CallApi<(int, int)>( useAsyncApi, this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. 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 1 column, but the value tuple type {typeof((int, int))} has 2 " + + "fields. Make sure that the SQL statement returns the same number of columns as the number of " + + "fields in the value tuple type.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) + public async Task QuerySingle_ValueTupleType_ShouldMaterializeBinaryData(bool useAsyncApi) { - var bytes = Generate.Single(); + var bytes = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(ValueTuple.Create(bytes)); + ) + ) + .Should() + .BeEquivalentTo(ValueTuple.Create(bytes)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QuerySingle_ValueTupleType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + ( + await CallApi<(long Id, DateTimeOffset DateTimeOffsetValue)>( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo((entity.Id, entity.DateTimeOffsetValue)); + ) + ) + .Should() + .BeEquivalentTo((entity.Id, entity.DateTimeOffsetValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task QuerySingle_ValueTupleType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -1287,13 +1388,7 @@ private static Task CallApi( try { return Task.FromResult( - connection.QuerySingle( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ) + connection.QuerySingle(statement, transaction, commandTimeout, commandType, cancellationToken) ); } catch (Exception ex) diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index 9c36fb3..00ccb5f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -2,127 +2,129 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_MySql : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultOfTTests_MySql + : DbConnectionExtensions_QuerySingleOrDefaultOfTTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_Oracle : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultOfTTests_Oracle + : DbConnectionExtensions_QuerySingleOrDefaultOfTTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_PostgreSql : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultOfTTests_PostgreSql + : DbConnectionExtensions_QuerySingleOrDefaultOfTTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_Sqlite : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultOfTTests_Sqlite + : DbConnectionExtensions_QuerySingleOrDefaultOfTTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests_SqlServer : - DbConnectionExtensions_QuerySingleOrDefaultOfTTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultOfTTests_SqlServer + : DbConnectionExtensions_QuerySingleOrDefaultOfTTests; -public abstract class - DbConnectionExtensions_QuerySingleOrDefaultOfTTests : IntegrationTestsBase< - TTestDatabaseProvider> +public abstract class DbConnectionExtensions_QuerySingleOrDefaultOfTTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT ''", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value '' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value '' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } - (await Invoking(() => - CallApi( + ( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, "SELECT 'ab'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'ab' ({typeof(String)}), " + - $"which could not be converted to the type {typeof(Char)}. See inner exception for details.*" - )) + $"The first column returned by the SQL statement contains the value 'ab' ({typeof(string)}), " + + $"which could not be converted to the type {typeof(char)}. See inner exception for details.*" + ) + ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_BuiltInType_CharTargetType_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(character); + ) + ) + .Should() + .Be(character); } [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingleOrDefault_BuiltInType_ColumnValueCannotBeConvertedToTargetType_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT 'A'", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The first column returned by the SQL statement contains the value 'A' ({typeof(String)}), which " + - $"could not be converted to the type {typeof(Int32)}. See inner exception for details.*" + $"The first column returned by the SQL statement contains the value 'A' ({typeof(string)}), which " + + $"could not be converted to the type {typeof(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi( @@ -132,17 +134,18 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value '999*' (System.*), which " + - $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" + "The first column returned by the SQL statement contains the value '999*' (System.*), which " + + $"could not be converted to the type {typeof(TestEnum)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi( @@ -152,102 +155,116 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains the value 'NonExistent' " + - $"({typeof(String)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + - "exception for details.*" + "The first column returned by the SQL statement contains the value 'NonExistent' " + + $"({typeof(string)}), which could not be converted to the type {typeof(TestEnum)}. See inner " + + "exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_BuiltInType_EnumTargetType_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{enumValue.ToString()}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(enumValue); + ) + ) + .Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingleOrDefault_BuiltInType_NonNullableTargetType_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => - CallApi( + CallApi( useAsyncApi, this.Connection, "SELECT NULL", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The first column returned by the SQL statement contains a NULL value, which could not be converted " + - $"to the type {typeof(Int32)}. 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(int)}. See inner exception for details.*" ); [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_BuiltInType_NullableTargetType_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi + bool useAsyncApi ) => - (await CallApi( - useAsyncApi, - this.Connection, - "SELECT NULL", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ( + await CallApi( + useAsyncApi, + this.Connection, + "SELECT NULL", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_BuiltInType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_BuiltInType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entity.DateTimeOffsetValue); + ) + ) + .Should() + .Be(entity.DateTimeOffsetValue); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -264,34 +281,39 @@ await Invoking(() => cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, "GetFirstEntity", commandType: CommandType.StoredProcedure, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -301,46 +323,49 @@ public async Task var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {TemporaryTable([entity])}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { @@ -354,16 +379,17 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be " + - $"converted to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be " + + $"converted to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } @@ -375,43 +401,46 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_EntityType_CharEntityProperty_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT '{character}' AS {Q("CharValue")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new Entity { CharValue = character }); + ) + ) + .Should() + .BeEquivalentTo(new Entity { CharValue = character }); } [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingleOrDefault_EntityType_ColumnDataTypeNotCompatibleWithEntityPropertyType_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => CallApi( @@ -421,28 +450,26 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" + "The data type System.* of the column 'TimeSpanValue' returned by the SQL statement is not " + + $"compatible with the property type {typeof(TimeSpan)} of the corresponding property of the type " + + $"{typeof(Entity)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_ColumnHasNoName_ShouldThrow(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_EntityType_ColumnHasNoName_ShouldThrow(bool useAsyncApi) { InterpolatedSqlStatement statement = this.TestDatabaseProvider switch { - SqlServerTestDatabaseProvider => - "SELECT 1", + SqlServerTestDatabaseProvider => "SELECT 1", - PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => - "SELECT 1 AS \" \"", + PostgreSqlTestDatabaseProvider or OracleTestDatabaseProvider => "SELECT 1 AS \" \"", - _ => - "SELECT 1 AS ''" + _ => "SELECT 1 AS ''", }; await Invoking(() => @@ -453,10 +480,11 @@ await Invoking(() => cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" + "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + + "statement returns have a name.*" ); } @@ -464,205 +492,230 @@ await Invoking(() => [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_EntityType_CompatiblePrivateConstructor_ShouldUsePrivateConstructor( - Boolean useAsyncApi + bool useAsyncApi ) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_EntityType_CompatiblePublicConstructor_ShouldUsePublicConstructor( - Boolean useAsyncApi + bool useAsyncApi ) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_EntityType_EntityTypeHasNoCorrespondingPropertyForColumn_ShouldIgnoreColumn( + bool useAsyncApi + ) { - var entity = (await Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", - cancellationToken: TestContext.Current.CancellationToken + var entity = ( + await Invoking(() => + CallApi( + useAsyncApi, + this.Connection, + $"SELECT 1 AS {Q("Id")}, 2 AS {Q("Int32Value")}, 3 AS {Q("NonExistent")}", + cancellationToken: TestContext.Current.CancellationToken + ) ) - ) - .Should().NotThrowAsync()).Subject; + .Should() + .NotThrowAsync() + ).Subject; - entity - .Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); + entity.Should().BeEquivalentTo(new Entity { Id = 1, Int32Value = 2 }); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_EntityType_EntityTypeWithPropertiesWithDifferentCasing_ShouldMaterializeEntities( + bool useAsyncApi + ) { var entity = this.CreateEntityInDb(); var entityWithDifferentCasingProperties = Generate.MapTo(entity); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entityWithDifferentCasingProperties); + ) + ) + .Should() + .BeEquivalentTo(entityWithDifferentCasingProperties); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi - ) => - await Invoking(() => CallApi( + public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidInteger_ShouldThrow( + bool useAsyncApi + ) => + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, 999 AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi - ) => - await Invoking(() => CallApi( + public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ColumnContainsInvalidString_ShouldThrow( + bool useAsyncApi + ) => + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, 'NonExistent' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertIntegerToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, - $"SELECT 1 AS {Q("Id")}, {(Int32)enumValue} AS {Q("Enum")}", + $"SELECT 1 AS {Q("Id")}, {(int)enumValue} AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken - ))! - .Enum - .Should().Be(enumValue); + ) + )! + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_EntityType_EnumEntityProperty_ShouldConvertStringToEnum(bool useAsyncApi) { var enumValue = Generate.Single(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT 1 AS {Q("Id")}, '{enumValue.ToString()}' AS {Q("Enum")}", cancellationToken: TestContext.Current.CancellationToken - ))! - .Enum - .Should().Be(enumValue); + ) + )! + .Enum.Should() + .Be(enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_EntityType_Mapping_Attributes_ShouldUseAttributesMapping(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo( + ) + ) + .Should() + .BeEquivalentTo( entity, - 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 QuerySingleOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_EntityType_Mapping_FluentApi_ShouldUseFluentApiMapping(bool useAsyncApi) { MappingTestEntityFluentApi.Configure(); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo( + ) + ) + .Should() + .BeEquivalentTo( entity, - 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")) ); } @@ -670,90 +723,92 @@ public async Task QuerySingleOrDefault_EntityType_Mapping_FluentApi_ShouldUseFlu [InlineData(false)] [InlineData(true)] public Task QuerySingleOrDefault_EntityType_NoCompatibleConstructor_NoParameterlessConstructor_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) => Invoking(() => - CallApi( - useAsyncApi, - this.Connection, - $"SELECT 1 AS {Q("NonExistent")}" - ) + CallApi(useAsyncApi, this.Connection, $"SELECT 1 AS {Q("NonExistent")}") ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + - "(* NonExistent).*" + $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + + "(* NonExistent).*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_EntityType_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties( + bool useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_EntityType_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties( + bool useAsyncApi + ) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_EntityType_NoMapping_ShouldUseEntityTypeNameAndPropertyNames( - Boolean useAsyncApi + bool useAsyncApi ) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("MappingTestEntity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingleOrDefault_EntityType_NonNullableEntityProperty_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => CallApi( @@ -763,10 +818,11 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding property of the type {typeof(Entity)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -774,44 +830,50 @@ Boolean useAsyncApi [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_EntityType_NullableEntityProperty_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi + bool useAsyncApi ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); + ) + ) + .Should() + .BeEquivalentTo(new Entity { Id = 1, NullableBooleanValue = null }); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_EntityType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_EntityType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingleOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task QuerySingleOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); @@ -825,7 +887,8 @@ public Task QuerySingleOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Boo cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); @@ -834,23 +897,26 @@ public Task QuerySingleOrDefault_EntityType_UnsupportedFieldType_ShouldThrow(Boo [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -859,228 +925,250 @@ public async Task QuerySingleOrDefault_Parameter_ShouldPassParameter(Boolean use ("Id", entity.Id) ); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_QueryReturnedMoreThanOneRow_ShouldThrow(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_QueryReturnedMoreThanOneRow_ShouldThrow(bool useAsyncApi) { this.CreateEntitiesInDb(2); - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did return more than one row." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did return more than one row."); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_QueryReturnedNoRows_ShouldThrow(bool useAsyncApi) { - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT {Q("Id")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(0); + ) + ) + .Should() + .Be(0); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ) + ) + .Should() + .BeNull(); - (await CallApi<(Int64, String)>( + ( + await CallApi<(long, string)>( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("StringValue")} FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(default); + ) + ) + .Should() + .Be(default); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityId = Generate.Id(); - InterpolatedSqlStatement statement = - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}"; + InterpolatedSqlStatement statement = $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable([entityId])}"; var temporaryTableName = statement.TemporaryTables[0].Name; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, statement, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(entityId); + ) + ) + .Should() + .Be(entityId); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entity = this.CreateEntityInDb(); var entityId = entity.Id; - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $""" - SELECT * - FROM {Q("Entity")} - WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable([entityId])}) - """, + SELECT * + FROM {Q("Entity")} + WHERE {Q("Id")} IN (SELECT {Q("Value")} FROM {TemporaryTable([entityId])}) + """, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ).Should().BeEquivalentTo(entity); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entity = this.CreateEntityInDb(transaction); - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", transaction, cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(entity); + ) + ) + .Should() + .BeEquivalentTo(entity); await transaction.RollbackAsync(); } - (await CallApi( + ( + await CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ) + ) + .Should() + .BeNull(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthNotOne_ShouldThrow( + bool useAsyncApi + ) { if (this.TestDatabaseProvider is not OracleTestDatabaseProvider) { // Oracle doesn't allow to return an empty string, because it treats empty strings as NULLs. await Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT '' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } await Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT 'ab' AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be " + - "exactly one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be " + + "exactly one character long." ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_ValueTupleType_CharValueTupleField_ColumnContainsStringWithLengthOne_ShouldGetFirstCharacter( + bool useAsyncApi + ) { - var character = Generate.Single(); + var character = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT '{character}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(character)); + ) + ) + .Should() + .Be(ValueTuple.Create(character)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task - QuerySingleOrDefault_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QuerySingleOrDefault_ValueTupleType_ColumnDataTypeNotCompatibleWithValueTupleFieldType_ShouldThrow( + bool useAsyncApi + ) => Invoking(() => CallApi>( useAsyncApi, @@ -1089,20 +1177,20 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + - $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}.*" + "The data type System.* of the column 'Value' returned by the SQL statement is not compatible with " + + $"the field type {typeof(TimeSpan)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task - QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidInteger_ShouldThrow( + bool useAsyncApi + ) => Invoking(() => CallApi>( useAsyncApi, @@ -1111,25 +1199,25 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - "Could not convert the value '999*' (System.*) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + "Could not convert the value '999*' (System.*) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public Task - QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ColumnContainsInvalidString_ShouldThrow( + bool useAsyncApi + ) => Invoking(() => CallApi>( useAsyncApi, @@ -1138,182 +1226,197 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Value' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException(typeof(InvalidCastException)) .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertIntegerToEnum( - Boolean useAsyncApi + bool useAsyncApi ) { var enumValue = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, - $"SELECT {(Int32)enumValue}", + $"SELECT {(int)enumValue}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(enumValue)); + ) + ) + .Should() + .Be(ValueTuple.Create(enumValue)); } [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_ValueTupleType_EnumValueTupleField_ShouldConvertStringToEnum( - Boolean useAsyncApi + bool useAsyncApi ) { var enumValue = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT '{enumValue}'", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(ValueTuple.Create(enumValue)); + ) + ) + .Should() + .Be(ValueTuple.Create(enumValue)); } [Theory] [InlineData(false)] [InlineData(true)] public Task QuerySingleOrDefault_ValueTupleType_NonNullableValueTupleField_ColumnContainsNull_ShouldThrow( - Boolean useAsyncApi + bool useAsyncApi ) { - this.Connection.ExecuteNonQuery( - $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)" - ); + this.Connection.ExecuteNonQuery($"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("BooleanValue")}) VALUES(1, NULL)"); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT {Q("BooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + - $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'BooleanValue' returned by the SQL statement contains a NULL value, but the " + + $"corresponding field of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_ValueTupleType_NullableValueTupleField_ColumnContainsNull_ShouldReturnNull( + bool useAsyncApi + ) { await this.Connection.ExecuteNonQueryAsync( $"INSERT INTO {Q("Entity")} ({Q("Id")}, {Q("NullableBooleanValue")}) VALUES(1, NULL)" ); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT {Q("NullableBooleanValue")} FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().Be(new(null)); + ) + ) + .Should() + .Be(new(null)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task - QuerySingleOrDefault_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( - Boolean useAsyncApi - ) => + public Task QuerySingleOrDefault_ValueTupleType_NumberOfColumnsDoesNotMatchNumberOfValueTupleFields_ShouldThrow( + bool useAsyncApi + ) => Invoking(() => - CallApi<(Int32, Int32)>( + CallApi<(int, int)>( useAsyncApi, this.Connection, "SELECT 1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The SQL statement returned 1 column, but the value tuple type {typeof((Int32, Int32))} has 2 " + - "fields. 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 1 column, but the value tuple type {typeof((int, int))} has 2 " + + "fields. Make sure that the SQL statement returns the same number of columns as the number of " + + "fields in the value tuple type.*" ); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_ValueTupleType_ShouldMaterializeBinaryData(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_ValueTupleType_ShouldMaterializeBinaryData(bool useAsyncApi) { - var bytes = Generate.Single(); + var bytes = Generate.Single(); - (await CallApi>( + ( + await CallApi>( useAsyncApi, this.Connection, $"SELECT {Parameter(bytes)} AS BinaryData", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo(ValueTuple.Create(bytes)); + ) + ) + .Should() + .BeEquivalentTo(ValueTuple.Create(bytes)); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_ValueTupleType_ShouldSupportDateTimeOffsetValues(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_ValueTupleType_ShouldSupportDateTimeOffsetValues(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsDateTimeOffset, ""); var entity = this.CreateEntityInDb(); - (await CallApi<(Int64 Id, DateTimeOffset DateTimeOffsetValue)>( + ( + await CallApi<(long Id, DateTimeOffset DateTimeOffsetValue)>( useAsyncApi, this.Connection, $"SELECT {Q("Id")}, {Q("DateTimeOffsetValue")} FROM {Q("EntityWithDateTimeOffset")}", cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeEquivalentTo((entity.Id, entity.DateTimeOffsetValue)); + ) + ) + .Should() + .BeEquivalentTo((entity.Id, entity.DateTimeOffsetValue)); } [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingleOrDefault_ValueTupleType_UnsupportedFieldType_ShouldThrow(Boolean useAsyncApi) + public Task QuerySingleOrDefault_ValueTupleType_UnsupportedFieldType_ShouldThrow(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.HasUnsupportedDataType, ""); var literal = this.TestDatabaseProvider.GetUnsupportedDataTypeLiteral(); return Invoking(() => - CallApi>( + CallApi>( useAsyncApi, this.Connection, $"SELECT {literal} AS {Q("Value")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( "The data type System.* of the column 'Value' returned by the SQL statement is not supported.*" ); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs index 097c44b..9878280 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs @@ -4,36 +4,30 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultTests_MySql : - DbConnectionExtensions_QuerySingleOrDefaultTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultTests_MySql + : DbConnectionExtensions_QuerySingleOrDefaultTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultTests_Oracle : - DbConnectionExtensions_QuerySingleOrDefaultTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultTests_Oracle + : DbConnectionExtensions_QuerySingleOrDefaultTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultTests_PostgreSql : - DbConnectionExtensions_QuerySingleOrDefaultTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultTests_PostgreSql + : DbConnectionExtensions_QuerySingleOrDefaultTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultTests_Sqlite : - DbConnectionExtensions_QuerySingleOrDefaultTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultTests_Sqlite + : DbConnectionExtensions_QuerySingleOrDefaultTests; -public sealed class - DbConnectionExtensions_QuerySingleOrDefaultTests_SqlServer : - DbConnectionExtensions_QuerySingleOrDefaultTests; +public sealed class DbConnectionExtensions_QuerySingleOrDefaultTests_SqlServer + : DbConnectionExtensions_QuerySingleOrDefaultTests; -public abstract class - DbConnectionExtensions_QuerySingleOrDefaultTests - : IntegrationTestsBase +public abstract class DbConnectionExtensions_QuerySingleOrDefaultTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_CancellationToken_ShouldCancelOperationIfCancellationIsRequested( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -50,14 +44,15 @@ await Invoking(() => cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); @@ -78,7 +73,7 @@ public async Task QuerySingleOrDefault_CommandType_ShouldUseCommandType(Boolean [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -98,17 +93,15 @@ Boolean useAsyncApi EntityAssertions.AssertDataRowMatchesEntity(dataRow!, entity); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -127,7 +120,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -144,7 +137,7 @@ public async Task QuerySingleOrDefault_InterpolatedParameter_ShouldPassInterpola [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -166,41 +159,44 @@ public async Task QuerySingleOrDefault_Parameter_ShouldPassParameter(Boolean use [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_QueryReturnedMoreThanOneRow_ShouldThrow(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_QueryReturnedMoreThanOneRow_ShouldThrow(bool useAsyncApi) { this.CreateEntitiesInDb(2); - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did return more than one row." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did return more than one row."); } - [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_QueryReturnedNoRows_ShouldReturnNull(Boolean useAsyncApi) => - ((Object?)await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + public async Task QuerySingleOrDefault_QueryReturnedNoRows_ShouldReturnNull(bool useAsyncApi) => + ( + (object?) + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); [Theory] [InlineData(false)] [InlineData(true)] public async Task QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -218,23 +214,19 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - dataRow - .Should().NotBeNull(); + dataRow.Should().NotBeNull(); - dataRow["Id"] - .Should().Be(entityId); + dataRow["Id"].Should().Be(entityId); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QuerySingleOrDefault_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -247,14 +239,13 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - ValueConverter.ConvertValueToType(dataRow!["Id"]) - .Should().Be(entityId); + ValueConverter.ConvertValueToType(dataRow!["Id"]).Should().Be(entityId); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_ShouldReturnDataRowForSingleRow(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_ShouldReturnDataRowForSingleRow(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -271,7 +262,7 @@ public async Task QuerySingleOrDefault_ShouldReturnDataRowForSingleRow(Boolean u [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingleOrDefault_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task QuerySingleOrDefault_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { @@ -290,17 +281,21 @@ public async Task QuerySingleOrDefault_Transaction_ShouldUseTransaction(Boolean await transaction.RollbackAsync(); } - ((Object?)await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - )) - .Should().BeNull(); + ( + (object?) + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + ) + .Should() + .BeNull(); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -323,13 +318,7 @@ public async Task QuerySingleOrDefault_Transaction_ShouldUseTransaction(Boolean try { return Task.FromResult( - connection.QuerySingleOrDefault( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ) + connection.QuerySingleOrDefault(statement, transaction, commandTimeout, commandType, cancellationToken) ); } catch (Exception ex) diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs index e56dc67..72f59e8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QuerySingleTests.cs @@ -4,34 +4,29 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QuerySingleTests_MySql : - DbConnectionExtensions_QuerySingleTests; +public sealed class DbConnectionExtensions_QuerySingleTests_MySql + : DbConnectionExtensions_QuerySingleTests; -public sealed class - DbConnectionExtensions_QuerySingleTests_Oracle : - DbConnectionExtensions_QuerySingleTests; +public sealed class DbConnectionExtensions_QuerySingleTests_Oracle + : DbConnectionExtensions_QuerySingleTests; -public sealed class - DbConnectionExtensions_QuerySingleTests_PostgreSql : - DbConnectionExtensions_QuerySingleTests; +public sealed class DbConnectionExtensions_QuerySingleTests_PostgreSql + : DbConnectionExtensions_QuerySingleTests; -public sealed class - DbConnectionExtensions_QuerySingleTests_Sqlite : - DbConnectionExtensions_QuerySingleTests; +public sealed class DbConnectionExtensions_QuerySingleTests_Sqlite + : DbConnectionExtensions_QuerySingleTests; -public sealed class - DbConnectionExtensions_QuerySingleTests_SqlServer : - DbConnectionExtensions_QuerySingleTests; +public sealed class DbConnectionExtensions_QuerySingleTests_SqlServer + : DbConnectionExtensions_QuerySingleTests; -public abstract class - DbConnectionExtensions_QuerySingleTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_QuerySingleTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task QuerySingle_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -47,14 +42,15 @@ await Invoking(() => cancellationToken: cancellationToken ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task QuerySingle_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); @@ -74,9 +70,7 @@ public async Task QuerySingle_CommandType_ShouldUseCommandType(Boolean useAsyncA [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution( - Boolean useAsyncApi - ) + public async Task QuerySingle_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -95,17 +89,15 @@ Boolean useAsyncApi EntityAssertions.AssertDataRowMatchesEntity(dataRow, entity); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QuerySingle_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -124,7 +116,7 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -141,7 +133,7 @@ public async Task QuerySingle_InterpolatedParameter_ShouldPassInterpolatedParame [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task QuerySingle_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -163,44 +155,43 @@ public async Task QuerySingle_Parameter_ShouldPassParameter(Boolean useAsyncApi) [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_QueryReturnedMoreThanOneRow_ShouldThrow(Boolean useAsyncApi) + public async Task QuerySingle_QueryReturnedMoreThanOneRow_ShouldThrow(bool useAsyncApi) { this.CreateEntitiesInDb(2); - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did return more than one row." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did return more than one row."); } - [Theory] [InlineData(false)] [InlineData(true)] - public Task QuerySingle_QueryReturnedNoRows_ShouldThrow(Boolean useAsyncApi) => - Invoking(() => CallApi( + public Task QuerySingle_QueryReturnedNoRows_ShouldThrow(bool useAsyncApi) => + Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = -1", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync() - .WithMessage( - "The SQL statement did not return any rows." - ); + .Should() + .ThrowAsync() + .WithMessage("The SQL statement did not return any rows."); [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(Boolean useAsyncApi) + public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterExecution(bool useAsyncApi) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -217,23 +208,19 @@ public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldDropTemporaryTabl cancellationToken: TestContext.Current.CancellationToken ); - dataRow - .Should().NotBeNull(); + dataRow.Should().NotBeNull(); - dataRow["Id"] - .Should().Be(entityId); + dataRow["Id"].Should().Be(entityId); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - QuerySingle_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi - ) + public async Task QuerySingle_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( + bool useAsyncApi + ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -246,14 +233,13 @@ Boolean useAsyncApi cancellationToken: TestContext.Current.CancellationToken ); - ValueConverter.ConvertValueToType(dataRow["Id"]) - .Should().Be(entityId); + ValueConverter.ConvertValueToType(dataRow["Id"]).Should().Be(entityId); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_ShouldReturnDataRowForSingleRow(Boolean useAsyncApi) + public async Task QuerySingle_ShouldReturnDataRowForSingleRow(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -270,7 +256,7 @@ public async Task QuerySingle_ShouldReturnDataRowForSingleRow(Boolean useAsyncAp [Theory] [InlineData(false)] [InlineData(true)] - public async Task QuerySingle_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task QuerySingle_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { @@ -289,18 +275,20 @@ public async Task QuerySingle_Transaction_ShouldUseTransaction(Boolean useAsyncA await transaction.RollbackAsync(); } - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, this.Connection, $"SELECT * FROM {Q("Entity")}", cancellationToken: TestContext.Current.CancellationToken ) ) - .Should().ThrowAsync(); + .Should() + .ThrowAsync(); } private static Task CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -311,25 +299,13 @@ private static Task CallApi( { if (useAsyncApi) { - return connection.QuerySingleAsync( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ); + return connection.QuerySingleAsync(statement, transaction, commandTimeout, commandType, cancellationToken); } try { return Task.FromResult( - connection.QuerySingle( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ) + connection.QuerySingle(statement, transaction, commandTimeout, commandType, cancellationToken) ); } catch (Exception ex) diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs index f058020..9f1d5d2 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.QueryTests.cs @@ -4,34 +4,29 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_QueryTests_MySql : - DbConnectionExtensions_QueryTests; +public sealed class DbConnectionExtensions_QueryTests_MySql + : DbConnectionExtensions_QueryTests; -public sealed class - DbConnectionExtensions_QueryTests_Oracle : - DbConnectionExtensions_QueryTests; +public sealed class DbConnectionExtensions_QueryTests_Oracle + : DbConnectionExtensions_QueryTests; -public sealed class - DbConnectionExtensions_QueryTests_PostgreSql : - DbConnectionExtensions_QueryTests; +public sealed class DbConnectionExtensions_QueryTests_PostgreSql + : DbConnectionExtensions_QueryTests; -public sealed class - DbConnectionExtensions_QueryTests_Sqlite : - DbConnectionExtensions_QueryTests; +public sealed class DbConnectionExtensions_QueryTests_Sqlite + : DbConnectionExtensions_QueryTests; -public sealed class - DbConnectionExtensions_QueryTests_SqlServer : - DbConnectionExtensions_QueryTests; +public sealed class DbConnectionExtensions_QueryTests_SqlServer + : DbConnectionExtensions_QueryTests; -public abstract class - DbConnectionExtensions_QueryTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_QueryTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(Boolean useAsyncApi) + public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsRequested(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -41,32 +36,36 @@ public async Task Query_CancellationToken_ShouldCancelOperationIfCancellationIsR await Invoking(() => CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: cancellationToken - ).ToListAsync(cancellationToken).AsTask() + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: cancellationToken + ) + .ToListAsync(cancellationToken) + .AsTask() ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task Query_CommandType_ShouldUseCommandType(bool useAsyncApi) { Assert.SkipUnless(this.TestDatabaseProvider.SupportsStoredProceduresReturningResultSet, ""); var entities = this.CreateEntitiesInDb(); var dataRows = await CallApi( - useAsyncApi, - this.Connection, - "GetEntities", - commandType: CommandType.StoredProcedure, - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); + useAsyncApi, + this.Connection, + "GetEntities", + commandType: CommandType.StoredProcedure, + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken); EntityAssertions.AssertDataRowsMatchEntities(dataRows, entities); } @@ -75,7 +74,7 @@ public async Task Query_CommandType_ShouldUseCommandType(Boolean useAsyncApi) [InlineData(false)] [InlineData(true)] public async Task Query_ComplexObjectsTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -87,38 +86,34 @@ Boolean useAsyncApi var temporaryTableName = statement.TemporaryTables[0].Name; var enumerator = CallApi( - useAsyncApi, - this.Connection, - statement, - cancellationToken: TestContext.Current.CancellationToken - ).GetAsyncEnumerator(); + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + ) + .GetAsyncEnumerator(); - (await enumerator.MoveNextAsync()) - .Should().BeTrue(); + (await enumerator.MoveNextAsync()).Should().BeTrue(); if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeTrue(); } - (await enumerator.MoveNextAsync()) - .Should().BeTrue(); + (await enumerator.MoveNextAsync()).Should().BeTrue(); - (await enumerator.MoveNextAsync()) - .Should().BeFalse(); + (await enumerator.MoveNextAsync()).Should().BeFalse(); await enumerator.DisposeAsync(); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] public async Task Query_ComplexObjectsTemporaryTable_ShouldPassInterpolatedObjectsAsMultiColumnTemporaryTable( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -126,11 +121,12 @@ Boolean useAsyncApi var entities = Generate.Multiple(); var dataRows = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); + useAsyncApi, + this.Connection, + $"SELECT * FROM {TemporaryTable(entities)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken); EntityAssertions.AssertDataRowsMatchEntities(dataRows, entities); } @@ -138,16 +134,17 @@ Boolean useAsyncApi [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_InterpolatedParameter_ShouldPassInterpolatedParameter(Boolean useAsyncApi) + public async Task Query_InterpolatedParameter_ShouldPassInterpolatedParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); var dataRows = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")} WHERE {Q("Id")} = {Parameter(entity.Id)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken); EntityAssertions.AssertDataRowsMatchEntities(dataRows, [entity]); } @@ -155,7 +152,7 @@ public async Task Query_InterpolatedParameter_ShouldPassInterpolatedParameter(Bo [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_Parameter_ShouldPassParameter(Boolean useAsyncApi) + public async Task Query_Parameter_ShouldPassParameter(bool useAsyncApi) { var entity = this.CreateEntityInDb(); @@ -165,11 +162,12 @@ public async Task Query_Parameter_ShouldPassParameter(Boolean useAsyncApi) ); var dataRows = await CallApi( - useAsyncApi, - this.Connection, - statement, - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken); EntityAssertions.AssertDataRowsMatchEntities(dataRows, [entity]); } @@ -178,7 +176,7 @@ public async Task Query_Parameter_ShouldPassParameter(Boolean useAsyncApi) [InlineData(false)] [InlineData(true)] public async Task Query_ScalarValuesTemporaryTable_ShouldDropTemporaryTableAfterEnumerationIsFinished( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -190,38 +188,34 @@ Boolean useAsyncApi var temporaryTableName = statement.TemporaryTables[0].Name; var enumerator = CallApi( - useAsyncApi, - this.Connection, - statement, - cancellationToken: TestContext.Current.CancellationToken - ).GetAsyncEnumerator(); + useAsyncApi, + this.Connection, + statement, + cancellationToken: TestContext.Current.CancellationToken + ) + .GetAsyncEnumerator(); - (await enumerator.MoveNextAsync()) - .Should().BeTrue(); + (await enumerator.MoveNextAsync()).Should().BeTrue(); if (this.TestDatabaseProvider.SupportsCommandExecutionWhileDataReaderIsOpen) { - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeTrue(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeTrue(); } - (await enumerator.MoveNextAsync()) - .Should().BeTrue(); + (await enumerator.MoveNextAsync()).Should().BeTrue(); - (await enumerator.MoveNextAsync()) - .Should().BeFalse(); + (await enumerator.MoveNextAsync()).Should().BeFalse(); await enumerator.DisposeAsync(); - this.ExistsTemporaryTableInDb(temporaryTableName) - .Should().BeFalse(); + this.ExistsTemporaryTableInDb(temporaryTableName).Should().BeFalse(); } [Theory] [InlineData(false)] [InlineData(true)] public async Task Query_ScalarValuesTemporaryTable_ShouldPassInterpolatedValuesAsSingleColumnTemporaryTable( - Boolean useAsyncApi + bool useAsyncApi ) { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -229,32 +223,33 @@ Boolean useAsyncApi var entityIds = Generate.Ids(); var dataRows = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); + useAsyncApi, + this.Connection, + $"SELECT {Q("Value")} AS {Q("Id")} FROM {TemporaryTable(entityIds)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken); for (var i = 0; i < entityIds.Count; i++) { - ValueConverter.ConvertValueToType(dataRows[i]["Id"]) - .Should().Be(entityIds[i]); + ValueConverter.ConvertValueToType(dataRows[i]["Id"]).Should().Be(entityIds[i]); } } [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_ShouldReturnDataRowsForQueryResult(Boolean useAsyncApi) + public async Task Query_ShouldReturnDataRowsForQueryResult(bool useAsyncApi) { var entities = this.CreateEntitiesInDb(); var dataRows = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken); EntityAssertions.AssertDataRowsMatchEntities(dataRows, entities); } @@ -262,36 +257,41 @@ public async Task Query_ShouldReturnDataRowsForQueryResult(Boolean useAsyncApi) [Theory] [InlineData(false)] [InlineData(true)] - public async Task Query_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task Query_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using (var transaction = await this.Connection.BeginTransactionAsync()) { var entities = this.CreateEntitiesInDb(null, transaction); var dataRows = await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken); + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + transaction, + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken); EntityAssertions.AssertDataRowsMatchEntities(dataRows, entities); await transaction.RollbackAsync(); } - (await CallApi( - useAsyncApi, - this.Connection, - $"SELECT * FROM {Q("Entity")}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEmpty(); + ( + await CallApi( + useAsyncApi, + this.Connection, + $"SELECT * FROM {Q("Entity")}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEmpty(); } private static IAsyncEnumerable CallApi( - Boolean useAsyncApi, + bool useAsyncApi, DbConnection connection, InterpolatedSqlStatement statement, DbTransaction? transaction = null, @@ -302,21 +302,11 @@ private static IAsyncEnumerable CallApi( { if (useAsyncApi) { - return connection.QueryAsync( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ); + return connection.QueryAsync(statement, transaction, commandTimeout, commandType, cancellationToken); } - return connection.Query( - statement, - transaction, - commandTimeout, - commandType, - cancellationToken - ).ToAsyncEnumerable(); + return connection + .Query(statement, transaction, commandTimeout, commandType, cancellationToken) + .ToAsyncEnumerable(); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.TemporaryTableTests.cs b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.TemporaryTableTests.cs index 1de3f5e..677235a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.TemporaryTableTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/DbConnectionExtensions.TemporaryTableTests.cs @@ -1,28 +1,23 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; -public sealed class - DbConnectionExtensions_TemporaryTableTests_MySql : - DbConnectionExtensions_TemporaryTableTests; +public sealed class DbConnectionExtensions_TemporaryTableTests_MySql + : DbConnectionExtensions_TemporaryTableTests; -public sealed class - DbConnectionExtensions_TemporaryTableTests_PostgreSql : - DbConnectionExtensions_TemporaryTableTests; +public sealed class DbConnectionExtensions_TemporaryTableTests_PostgreSql + : DbConnectionExtensions_TemporaryTableTests; -public sealed class - DbConnectionExtensions_TemporaryTableTests_Sqlite : - DbConnectionExtensions_TemporaryTableTests; +public sealed class DbConnectionExtensions_TemporaryTableTests_Sqlite + : DbConnectionExtensions_TemporaryTableTests; -public sealed class - DbConnectionExtensions_TemporaryTableTests_SqlServer : - DbConnectionExtensions_TemporaryTableTests; +public sealed class DbConnectionExtensions_TemporaryTableTests_SqlServer + : DbConnectionExtensions_TemporaryTableTests; -public abstract class - DbConnectionExtensions_TemporaryTableTests : IntegrationTestsBase +public abstract class DbConnectionExtensions_TemporaryTableTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Fact] - public void - TemporaryTable_ComplexObjects_EnumProperty_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() + public async Task TemporaryTableAsync_ComplexObjects_EnumProperty_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -30,15 +25,20 @@ public void var entities = Generate.Multiple(); - this.Connection.Query( - $"SELECT {Q("Enum")} FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum)); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Enum")} FROM {TemporaryTable(entities)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities.Select(a => (int)a.Enum)); } [Fact] - public void TemporaryTable_ComplexObjects_EnumProperty_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() + public async Task TemporaryTableAsync_ComplexObjects_EnumProperty_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -46,29 +46,39 @@ public void TemporaryTable_ComplexObjects_EnumProperty_EnumSerializationModeIsSt var entities = Generate.Multiple(); - this.Connection.Query( - $"SELECT {Q("Enum")} FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Enum")} FROM {TemporaryTable(entities)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities.Select(a => a.Enum.ToString())); } [Fact] - public void TemporaryTable_ComplexObjects_ShouldBePassedAsMultiColumnTemporaryTableToSqlStatement() + public async Task TemporaryTableAsync_ComplexObjects_ShouldBePassedAsMultiColumnTemporaryTableToSqlStatement() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(); - this.Connection.Query( - $"SELECT * FROM {TemporaryTable(entities)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entities); + ( + await this + .Connection.QueryAsync( + $"SELECT * FROM {TemporaryTable(entities)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entities); } [Fact] - public void TemporaryTable_ScalarValues_Enums_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() + public async Task TemporaryTableAsync_ScalarValues_Enums_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -76,16 +86,20 @@ public void TemporaryTable_ScalarValues_Enums_EnumSerializationModeIsIntegers_Sh var enumValues = Generate.Multiple(); - this.Connection - .Query( - $"SELECT {Q("Value")} FROM {TemporaryTable(enumValues)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(enumValues.Select(a => (Int32)a)); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Value")} FROM {TemporaryTable(enumValues)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(enumValues.Select(a => (int)a)); } [Fact] - public void TemporaryTable_ScalarValues_Enums_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() + public async Task TemporaryTableAsync_ScalarValues_Enums_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -93,32 +107,39 @@ public void TemporaryTable_ScalarValues_Enums_EnumSerializationModeIsStrings_Sho var enumValues = Generate.Multiple(); - this.Connection - .Query( - $"SELECT {Q("Value")} FROM {TemporaryTable(enumValues)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(enumValues.Select(a => a.ToString())); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Value")} FROM {TemporaryTable(enumValues)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(enumValues.Select(a => a.ToString())); } [Fact] - public void TemporaryTable_ScalarValues_ShouldBePassedAsSingleColumnTemporaryTableToSqlStatement() + public async Task TemporaryTableAsync_ScalarValues_ShouldBePassedAsSingleColumnTemporaryTableToSqlStatement() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(); - this.Connection - .Query( - $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", - cancellationToken: TestContext.Current.CancellationToken - ) - .Should().BeEquivalentTo(entityIds); + ( + await this + .Connection.QueryAsync( + $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .ToListAsync(TestContext.Current.CancellationToken) + ) + .Should() + .BeEquivalentTo(entityIds); } [Fact] - public async Task - TemporaryTableAsync_ComplexObjects_EnumProperty_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() + public void TemporaryTable_ComplexObjects_EnumProperty_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -126,16 +147,16 @@ public async Task var entities = Generate.Multiple(); - (await this.Connection.QueryAsync( + this.Connection.Query( $"SELECT {Q("Enum")} FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities.Select(a => (Int32)a.Enum)); + ) + .Should() + .BeEquivalentTo(entities.Select(a => (int)a.Enum)); } [Fact] - public async Task - TemporaryTableAsync_ComplexObjects_EnumProperty_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() + public void TemporaryTable_ComplexObjects_EnumProperty_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -143,30 +164,31 @@ public async Task var entities = Generate.Multiple(); - (await this.Connection.QueryAsync( + this.Connection.Query( $"SELECT {Q("Enum")} FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities.Select(a => a.Enum.ToString())); + ) + .Should() + .BeEquivalentTo(entities.Select(a => a.Enum.ToString())); } [Fact] - public async Task TemporaryTableAsync_ComplexObjects_ShouldBePassedAsMultiColumnTemporaryTableToSqlStatement() + public void TemporaryTable_ComplexObjects_ShouldBePassedAsMultiColumnTemporaryTableToSqlStatement() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entities = Generate.Multiple(); - (await this.Connection.QueryAsync( + this.Connection.Query( $"SELECT * FROM {TemporaryTable(entities)}", cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entities); + ) + .Should() + .BeEquivalentTo(entities); } [Fact] - public async Task - TemporaryTableAsync_ScalarValues_Enums_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() + public void TemporaryTable_ScalarValues_Enums_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -174,17 +196,16 @@ public async Task var enumValues = Generate.Multiple(); - (await this.Connection - .QueryAsync( - $"SELECT {Q("Value")} FROM {TemporaryTable(enumValues)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(enumValues.Select(a => (Int32)a)); + this.Connection.Query( + $"SELECT {Q("Value")} FROM {TemporaryTable(enumValues)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .Should() + .BeEquivalentTo(enumValues.Select(a => (int)a)); } [Fact] - public async Task - TemporaryTableAsync_ScalarValues_Enums_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() + public void TemporaryTable_ScalarValues_Enums_EnumSerializationModeIsStrings_ShouldSerializeEnumToString() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); @@ -192,26 +213,26 @@ public async Task var enumValues = Generate.Multiple(); - (await this.Connection - .QueryAsync( - $"SELECT {Q("Value")} FROM {TemporaryTable(enumValues)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(enumValues.Select(a => a.ToString())); + this.Connection.Query( + $"SELECT {Q("Value")} FROM {TemporaryTable(enumValues)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .Should() + .BeEquivalentTo(enumValues.Select(a => a.ToString())); } [Fact] - public async Task TemporaryTableAsync_ScalarValues_ShouldBePassedAsSingleColumnTemporaryTableToSqlStatement() + public void TemporaryTable_ScalarValues_ShouldBePassedAsSingleColumnTemporaryTableToSqlStatement() { Assert.SkipUnless(this.DatabaseAdapter.SupportsTemporaryTables(this.Connection), ""); var entityIds = Generate.Ids(); - (await this.Connection - .QueryAsync( - $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", - cancellationToken: TestContext.Current.CancellationToken - ).ToListAsync(TestContext.Current.CancellationToken)) - .Should().BeEquivalentTo(entityIds); + this.Connection.Query( + $"SELECT {Q("Value")} FROM {TemporaryTable(entityIds)}", + cancellationToken: TestContext.Current.CancellationToken + ) + .Should() + .BeEquivalentTo(entityIds); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs index 5c98443..81ac856 100644 --- a/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs +++ b/tests/DbConnectionPlus.IntegrationTests/GlobalUsings.cs @@ -1,12 +1,11 @@ global using System.Data; -global using Xunit; global using AwesomeAssertions; +global using static AwesomeAssertions.FluentActions; global using Microsoft.Data.SqlClient; global using RentADeveloper.DbConnectionPlus.Configuration; global using RentADeveloper.DbConnectionPlus.DbCommands; +global using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; global using RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; global using RentADeveloper.DbConnectionPlus.SqlStatements; global using RentADeveloper.DbConnectionPlus.UnitTests.TestData; global using DataRow = RentADeveloper.DbConnectionPlus.Dynamic.DataRow; -global using static AwesomeAssertions.FluentActions; -global using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; diff --git a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs index 55a7c0e..e702a19 100644 --- a/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs +++ b/tests/DbConnectionPlus.IntegrationTests/IntegrationTestsBase.cs @@ -31,16 +31,39 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests; /// that before this constructor opens a connection to it. /// public abstract class IntegrationTestsBase - : IClassFixture>, IDisposable, IAsyncDisposable + : IClassFixture>, + IDisposable, + IAsyncDisposable where TTestDatabaseProvider : ITestDatabaseProvider, new() { + /// + /// The database adapter for the test database of the currently running integration test. + /// + /// + /// The adapter rather than the provider: an interface that declares a static abstract member - which + /// is - cannot be used as a type argument. + /// +#pragma warning disable S2743 + private static readonly AsyncLocal currentDatabaseAdapter = new(); +#pragma warning restore S2743 + + /// + /// The connection to the test database for the currently running integration test. + /// +#pragma warning disable S2743 + private static readonly AsyncLocal currentTestDatabaseConnection = new(); +#pragma warning restore S2743 + + private bool logDbCommands; + protected IntegrationTestsBase() { // Ensure consistent culture for tests. CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = - Thread.CurrentThread.CurrentCulture = - Thread.CurrentThread.CurrentUICulture = new("en-US"); + Thread.CurrentThread.CurrentCulture = + Thread.CurrentThread.CurrentUICulture = + new("en-US"); this.logDbCommands = false; @@ -48,12 +71,14 @@ protected IntegrationTestsBase() DbConnectionPlusConfiguration.Instance = new() { EnumSerializationMode = EnumSerializationMode.Strings, - InterceptDbCommand = this.InterceptDbCommand + InterceptDbCommand = this.InterceptDbCommand, }; DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(new MySqlDatabaseAdapter()); DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(new OracleDatabaseAdapter()); - DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(new PostgreSqlDatabaseAdapter()); + DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter( + new PostgreSqlDatabaseAdapter() + ); DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(new SqliteDatabaseAdapter()); DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(new SqlServerDatabaseAdapter()); @@ -79,25 +104,22 @@ protected IntegrationTestsBase() /// DbConnectionPlus. Subsequent commands will not be delayed unless this property is set to /// again. /// - public Boolean DelayNextDbCommand { get; set; } + public bool DelayNextDbCommand { get; set; } - /// - public void Dispose() - { - GC.SuppressFinalize(this); - - this.Connection.Close(); - this.Connection.Dispose(); - } + /// + /// The connection to the test database. + /// + protected DbConnection Connection { get; } - /// - public async ValueTask DisposeAsync() - { - GC.SuppressFinalize(this); + /// + /// The DbConnectionPlus database adapter for the test database. + /// + protected IDatabaseAdapter DatabaseAdapter => this.TestDatabaseProvider.DatabaseAdapter; - await this.Connection.CloseAsync(); - await this.Connection.DisposeAsync(); - } + /// + /// The provider for the test database. + /// + protected TTestDatabaseProvider TestDatabaseProvider { get; } /// /// Returns the specified parameter name with the appropriate prefix (e.g. "@" for SQL Server or ":" for Oracle) @@ -108,8 +130,7 @@ public async ValueTask DisposeAsync() /// The formatted parameter name, including the appropriate prefix, suitable for inclusion in SQL statements. /// /// The name of this method is intentionally kept very short, so test code doesn't get bloated. - public static String P(String parameterName) => - currentDatabaseAdapter.Value!.FormatParameterName(parameterName); + public static string P(string parameterName) => currentDatabaseAdapter.Value!.FormatParameterName(parameterName); /// /// Returns the specified database identifier properly quoted for use in SQL statements according to the current @@ -118,8 +139,7 @@ public static String P(String parameterName) => /// The identifier to quote. /// The quoted identifier, suitable for inclusion in SQL statements. /// The name of this method is intentionally kept very short, so test code doesn't get bloated. - public static String Q(String identifier) => - currentDatabaseAdapter.Value!.QuoteIdentifier(identifier); + public static string Q(string identifier) => currentDatabaseAdapter.Value!.QuoteIdentifier(identifier); /// /// Returns the specified temporary table name properly quoted for use in SQL statements according to the current @@ -128,26 +148,39 @@ public static String Q(String identifier) => /// The name of the temporary table to quote. /// The quoted temporary table name, suitable for inclusion in SQL statements. /// The name of this method is intentionally kept very short, so test code doesn't get bloated. - public static String QT(String tableName) => - currentDatabaseAdapter.Value!.QuoteTemporaryTableName( - tableName, - currentTestDatabaseConnection.Value! - ); + public static string QT(string tableName) => + currentDatabaseAdapter.Value!.QuoteTemporaryTableName(tableName, currentTestDatabaseConnection.Value!); - /// - /// The connection to the test database. - /// - protected DbConnection Connection { get; } + /// + public void Dispose() + { + GC.SuppressFinalize(this); - /// - /// The DbConnectionPlus database adapter for the test database. - /// - protected IDatabaseAdapter DatabaseAdapter => this.TestDatabaseProvider.DatabaseAdapter; + this.Connection.Close(); + this.Connection.Dispose(); + } + + /// + public async ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + + await this.Connection.CloseAsync(); + await this.Connection.DisposeAsync(); + } /// - /// The provider for the test database. + /// Creates a that will be cancelled after 100 milliseconds. /// - protected TTestDatabaseProvider TestDatabaseProvider { get; } + /// A that will be cancelled after 100 milliseconds. + protected static CancellationToken CreateCancellationTokenThatIsCancelledAfter100Milliseconds() + { +#pragma warning disable S2930 + var cancellationTokenSource = new CancellationTokenSource(); +#pragma warning restore S2930 + cancellationTokenSource.CancelAfter(100); + return cancellationTokenSource.Token; + } /// /// Creates the specified number of entities of the type and inserts them into the test @@ -160,28 +193,22 @@ public static String QT(String tableName) => /// /// The database transaction within to perform the operation. /// The entities that were created and inserted. - protected List CreateEntitiesInDb(Int32? numberOfEntities = null, DbTransaction? transaction = null) + protected List CreateEntitiesInDb(int? numberOfEntities = null, DbTransaction? transaction = null) where T : class => this.ExecuteWithoutDbCommandLogging(() => - { - var entities = Generate.Multiple(numberOfEntities); + { + var entities = Generate.Multiple(numberOfEntities); - this.Connection.InsertEntities( - entities, - transaction, - TestContext.Current.CancellationToken - ); + this.Connection.InsertEntities(entities, transaction, TestContext.Current.CancellationToken); - foreach (var entity in entities) - { - // Verify that the entity has been inserted: - this.ExistsEntityInDb(entity, transaction) - .Should().BeTrue(); - } - - return entities; + foreach (var entity in entities) + { + // Verify that the entity has been inserted: + this.ExistsEntityInDb(entity, transaction).Should().BeTrue(); } - ); + + return entities; + }); /// /// Creates an entity of the type and inserts it into the test database. @@ -190,25 +217,18 @@ protected List CreateEntitiesInDb(Int32? numberOfEntities = null, DbTransa /// The database transaction within to perform the operation. /// The entity that was created and inserted. protected T CreateEntityInDb(DbTransaction? transaction = null) - where T : class - => - this.ExecuteWithoutDbCommandLogging(() => - { - var entity = Generate.Single(); + where T : class => + this.ExecuteWithoutDbCommandLogging(() => + { + var entity = Generate.Single(); - this.Connection.InsertEntity( - entity, - transaction, - TestContext.Current.CancellationToken - ); + this.Connection.InsertEntity(entity, transaction, TestContext.Current.CancellationToken); - // Verify that the entity has been inserted: - this.ExistsEntityInDb(entity, transaction) - .Should().BeTrue(); + // Verify that the entity has been inserted: + this.ExistsEntityInDb(entity, transaction).Should().BeTrue(); - return entity; - } - ); + return entity; + }); /// /// Determines whether an entity having the key(s) of the specified entity exists in the test database. @@ -221,7 +241,7 @@ protected T CreateEntityInDb(DbTransaction? transaction = null) /// . /// /// - protected Boolean ExistsEntityInDb(T entity, DbTransaction? transaction = null) + protected bool ExistsEntityInDb(T entity, DbTransaction? transaction = null) where T : class { var metadata = EntityHelper.GetEntityTypeMetadata(typeof(T)); @@ -236,7 +256,7 @@ protected Boolean ExistsEntityInDb(T entity, DbTransaction? transaction = nul $""" SELECT 1 FROM {Q(metadata.TableName)} - WHERE {String.Join( + WHERE {string.Join( " AND ", keyProperties.Select(p => $"{Q(p.ColumnName)} = {P(p.PropertyName)}").ToList() )} @@ -244,11 +264,8 @@ SELECT 1 keyProperties.Select(p => (p.PropertyName, p.PropertyGetter!(entity))).ToArray()! ); - return this.ExecuteWithoutDbCommandLogging(() => this.Connection.Exists( - statement, - transaction, - cancellationToken: TestContext.Current.CancellationToken - ) + return this.ExecuteWithoutDbCommandLogging(() => + this.Connection.Exists(statement, transaction, cancellationToken: TestContext.Current.CancellationToken) ); } @@ -261,13 +278,9 @@ SELECT 1 /// if a temporary table with the specified name exists in the test database; /// otherwise, . /// - protected Boolean ExistsTemporaryTableInDb(String tableName, DbTransaction? transaction = null) => + protected bool ExistsTemporaryTableInDb(string tableName, DbTransaction? transaction = null) => this.ExecuteWithoutDbCommandLogging(() => - this.TestDatabaseProvider.ExistsTemporaryTable( - tableName, - this.Connection, - transaction - ) + this.TestDatabaseProvider.ExistsTemporaryTable(tableName, this.Connection, transaction) ); /// @@ -276,7 +289,7 @@ protected Boolean ExistsTemporaryTableInDb(String tableName, DbTransaction? tran /// The name of the temporary table that contains the specified column. /// The name of the column of which to get the collation. /// The collation of the specified column of the specified temporary table. - protected String GetCollationOfTemporaryTableColumn(String temporaryTableName, String columnName) => + protected string GetCollationOfTemporaryTableColumn(string temporaryTableName, string columnName) => this.ExecuteWithoutDbCommandLogging(() => this.TestDatabaseProvider.GetCollationOfTemporaryTableColumn( temporaryTableName, @@ -291,16 +304,9 @@ protected String GetCollationOfTemporaryTableColumn(String temporaryTableName, S /// The name of the temporary table that contains the specified column. /// The name of the column of which to get the data type. /// The data type of the specified column of the specified temporary table. - protected String GetDataTypeOfTemporaryTableColumn( - String temporaryTableName, - String columnName - ) => + protected string GetDataTypeOfTemporaryTableColumn(string temporaryTableName, string columnName) => this.ExecuteWithoutDbCommandLogging(() => - this.TestDatabaseProvider.GetDataTypeOfTemporaryTableColumn( - temporaryTableName, - columnName, - this.Connection - ) + this.TestDatabaseProvider.GetDataTypeOfTemporaryTableColumn(temporaryTableName, columnName, this.Connection) ); /// @@ -327,7 +333,7 @@ private void InterceptDbCommand(DbCommand command, IReadOnlyList - /// Creates a that will be cancelled after 100 milliseconds. - /// - /// A that will be cancelled after 100 milliseconds. - protected static CancellationToken CreateCancellationTokenThatIsCancelledAfter100Milliseconds() - { -#pragma warning disable S2930 - var cancellationTokenSource = new CancellationTokenSource(); -#pragma warning restore S2930 - cancellationTokenSource.CancelAfter(100); - return cancellationTokenSource.Token; - } - - private Boolean logDbCommands; - - /// - /// The connection to the test database for the currently running integration test. - /// -#pragma warning disable S2743 - private static readonly AsyncLocal currentTestDatabaseConnection = new(); -#pragma warning restore S2743 - - /// - /// The database adapter for the test database of the currently running integration test. - /// - /// - /// The adapter rather than the provider: an interface that declares a static abstract member - which - /// is - cannot be used as a type argument. - /// -#pragma warning disable S2743 - private static readonly AsyncLocal currentDatabaseAdapter = new(); -#pragma warning restore S2743 } diff --git a/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs b/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs index 67facb9..54d04d8 100644 --- a/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs +++ b/tests/DbConnectionPlus.IntegrationTests/Readers/CommandDisposingDataReaderDecoratorTests.cs @@ -2,32 +2,27 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.Readers; -public sealed class - CommandDisposingDataReaderDecoratorTests_MySql : - CommandDisposingDataReaderDecoratorTests; +public sealed class CommandDisposingDataReaderDecoratorTests_MySql + : CommandDisposingDataReaderDecoratorTests; -public sealed class - CommandDisposingDataReaderDecoratorTests_Oracle : - CommandDisposingDataReaderDecoratorTests; +public sealed class CommandDisposingDataReaderDecoratorTests_Oracle + : CommandDisposingDataReaderDecoratorTests; -public sealed class - CommandDisposingDataReaderDecoratorTests_PostgreSql : - CommandDisposingDataReaderDecoratorTests; +public sealed class CommandDisposingDataReaderDecoratorTests_PostgreSql + : CommandDisposingDataReaderDecoratorTests; -public sealed class - CommandDisposingDataReaderDecoratorTests_Sqlite : - CommandDisposingDataReaderDecoratorTests; +public sealed class CommandDisposingDataReaderDecoratorTests_Sqlite + : CommandDisposingDataReaderDecoratorTests; -public sealed class - CommandDisposingDataReaderDecoratorTests_SqlServer : - CommandDisposingDataReaderDecoratorTests; +public sealed class CommandDisposingDataReaderDecoratorTests_SqlServer + : CommandDisposingDataReaderDecoratorTests; -public abstract class - CommandDisposingDataReaderDecoratorTests : IntegrationTestsBase +public abstract class CommandDisposingDataReaderDecoratorTests + : IntegrationTestsBase where TTestDatabaseProvider : ITestDatabaseProvider, new() { [Fact] - public void Read_OperationCancelledViaCancellationToken_ShouldThrowOperationCanceledException() + public async Task ReadAsync_OperationCancelledViaCancellationToken_ShouldThrowOperationCanceledException() { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -36,26 +31,25 @@ public void Read_OperationCancelledViaCancellationToken_ShouldThrowOperationCanc // meaning the cancellation won't be observed until after the delay. Assert.SkipWhen(this.TestDatabaseProvider is PostgreSqlTestDatabaseProvider, ""); - using var command = this.Connection.CreateCommand(); + await using var command = this.Connection.CreateCommand(); command.CommandText = "SELECT 1; " + this.TestDatabaseProvider.DelayTwoSecondsStatement + " SELECT 1;"; using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; ThreadPool.QueueUserWorkItem(_ => - { - Thread.Sleep(100); - cancellationTokenSource.Cancel(); - // ReSharper disable once AccessToDisposedClosure - command.Cancel(); - } - ); + { + Thread.Sleep(100); + cancellationTokenSource.Cancel(); + // ReSharper disable once AccessToDisposedClosure + command.Cancel(); + }); var commandDisposer = new DbCommandDisposer(command, [], default); - using var decoratedReader = command.ExecuteReader(); + await using var decoratedReader = await command.ExecuteReaderAsync(TestContext.Current.CancellationToken); - using var decorator = new CommandDisposingDataReaderDecorator( + await using var decorator = new CommandDisposingDataReaderDecorator( decoratedReader, this.DatabaseAdapter, commandDisposer, @@ -63,18 +57,20 @@ public void Read_OperationCancelledViaCancellationToken_ShouldThrowOperationCanc ); // Read the value from before the delay: - decorator.Read() - .Should().BeTrue(); + (await decorator.ReadAsync(TestContext.Current.CancellationToken)) + .Should() + .BeTrue(); // The next read should be cancelled: // ReSharper disable once AccessToDisposedClosure - Invoking(() => decorator.Read()) - .Should().Throw() + await Invoking(() => decorator.ReadAsync(cancellationToken)) + .Should() + .ThrowAsync() .Where(a => a.CancellationToken == cancellationToken); } [Fact] - public async Task ReadAsync_OperationCancelledViaCancellationToken_ShouldThrowOperationCanceledException() + public void Read_OperationCancelledViaCancellationToken_ShouldThrowOperationCanceledException() { Assert.SkipUnless(this.TestDatabaseProvider.SupportsProperCommandCancellation, ""); @@ -83,26 +79,25 @@ public async Task ReadAsync_OperationCancelledViaCancellationToken_ShouldThrowOp // meaning the cancellation won't be observed until after the delay. Assert.SkipWhen(this.TestDatabaseProvider is PostgreSqlTestDatabaseProvider, ""); - await using var command = this.Connection.CreateCommand(); + using var command = this.Connection.CreateCommand(); command.CommandText = "SELECT 1; " + this.TestDatabaseProvider.DelayTwoSecondsStatement + " SELECT 1;"; using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; ThreadPool.QueueUserWorkItem(_ => - { - Thread.Sleep(100); - cancellationTokenSource.Cancel(); - // ReSharper disable once AccessToDisposedClosure - command.Cancel(); - } - ); + { + Thread.Sleep(100); + cancellationTokenSource.Cancel(); + // ReSharper disable once AccessToDisposedClosure + command.Cancel(); + }); var commandDisposer = new DbCommandDisposer(command, [], default); - await using var decoratedReader = await command.ExecuteReaderAsync(TestContext.Current.CancellationToken); + using var decoratedReader = command.ExecuteReader(); - await using var decorator = new CommandDisposingDataReaderDecorator( + using var decorator = new CommandDisposingDataReaderDecorator( decoratedReader, this.DatabaseAdapter, commandDisposer, @@ -110,13 +105,13 @@ public async Task ReadAsync_OperationCancelledViaCancellationToken_ShouldThrowOp ); // Read the value from before the delay: - (await decorator.ReadAsync(TestContext.Current.CancellationToken)) - .Should().BeTrue(); + decorator.Read().Should().BeTrue(); // The next read should be cancelled: // ReSharper disable once AccessToDisposedClosure - await Invoking(() => decorator.ReadAsync(cancellationToken)) - .Should().ThrowAsync() + Invoking(() => decorator.Read()) + .Should() + .Throw() .Where(a => a.CancellationToken == cancellationToken); } } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/ITestDatabaseContainerFixture.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/ITestDatabaseContainerFixture.cs index cef66c9..4266f7f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/ITestDatabaseContainerFixture.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/ITestDatabaseContainerFixture.cs @@ -15,5 +15,5 @@ internal interface ITestDatabaseContainerFixture : IAsyncLifetime /// The container publishes its port to a free port of the host, so this is only known once the container /// has been started. /// - public String ConnectionString { get; } + public string ConnectionString { get; } } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/MySqlContainerFixture.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/MySqlContainerFixture.cs index 0c7ebeb..feb675f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/MySqlContainerFixture.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/MySqlContainerFixture.cs @@ -12,10 +12,15 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase.Containe /// Runs the MySQL server the MySQL integration tests use in a Docker container. /// internal sealed class MySqlContainerFixture() - : DbContainerFixture(TestDatabaseDiagnosticMessageSink.Instance), ITestDatabaseContainerFixture + : DbContainerFixture(TestDatabaseDiagnosticMessageSink.Instance), + ITestDatabaseContainerFixture { + private const string Image = "mysql:latest"; + + private const string RootUsername = "root"; + /// - public override String ConnectionString => + public override string ConnectionString => new MySqlConnectionStringBuilder { Server = this.Container.Hostname, @@ -25,12 +30,11 @@ internal sealed class MySqlContainerFixture() // MySqlTemporaryTableBuilder fills temporary tables with MySqlBulkCopy, which is LOAD DATA LOCAL // INFILE underneath and refuses to run unless the client allows it. - AllowLoadLocalInfile = true + AllowLoadLocalInfile = true, }.ConnectionString; /// - public override DbProviderFactory DbProviderFactory => - MySqlConnectorFactory.Instance; + public override DbProviderFactory DbProviderFactory => MySqlConnectorFactory.Instance; /// protected override MySqlBuilder Configure() => @@ -39,8 +43,4 @@ protected override MySqlBuilder Configure() => new MySqlBuilder(Image) .WithUsername(RootUsername) .WithPassword(TestDatabaseContainers.Password); - - private const String Image = "mysql:latest"; - - private const String RootUsername = "root"; } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/OracleContainerFixture.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/OracleContainerFixture.cs index 05176cd..2f6b5ef 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/OracleContainerFixture.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/OracleContainerFixture.cs @@ -12,51 +12,49 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase.Containe /// Runs the Oracle server the Oracle integration tests use in a Docker container. /// internal sealed class OracleContainerFixture() - : DbContainerFixture(TestDatabaseDiagnosticMessageSink.Instance), ITestDatabaseContainerFixture + : DbContainerFixture(TestDatabaseDiagnosticMessageSink.Instance), + ITestDatabaseContainerFixture { + /// + /// The image the container runs. + /// + /// + /// The faststart variants carry an already created database and come up in well under a minute, where the + /// plain image spends several minutes creating FREEPDB1 on first start. The tag has to name the major + /// version: the module reads it to decide that this image serves FREEPDB1 rather than XEPDB1. + /// + private const string Image = "gvenzl/oracle-free:23-slim-faststart"; + + private const string ServiceName = "FREEPDB1"; + + private const string SystemUsername = "SYSTEM"; + /// /// /// The tests connect as SYSTEM, not as the unprivileged application user the module creates: the /// command-timeout tests call DBMS_LOCK.SLEEP, on which that user holds no EXECUTE grant. /// sets the password of both accounts. /// - public override String ConnectionString => + public override string ConnectionString => new OracleConnectionStringBuilder { DataSource = $"{this.Container.Hostname}:{this.MappedPort}/{ServiceName}", UserID = SystemUsername, - Password = TestDatabaseContainers.Password + Password = TestDatabaseContainers.Password, }.ConnectionString; /// - public override DbProviderFactory DbProviderFactory => - OracleClientFactory.Instance; + public override DbProviderFactory DbProviderFactory => OracleClientFactory.Instance; + + /// + /// The host port the container's Oracle listener is published on. + /// + private ushort MappedPort => this.Container.GetMappedPublicPort(OracleBuilder.OraclePort); /// protected override OracleBuilder Configure() => // WithDatabase is deliberately not called: for an Oracle 18+ image the module would only pass the name // on to ORACLE_DATABASE if it differed from the pluggable database the image already ships, and asking // this one to create a second FREEPDB1 fails. - new OracleBuilder(Image) - .WithPassword(TestDatabaseContainers.Password); - - /// - /// The host port the container's Oracle listener is published on. - /// - private UInt16 MappedPort => - this.Container.GetMappedPublicPort(OracleBuilder.OraclePort); - - /// - /// The image the container runs. - /// - /// - /// The faststart variants carry an already created database and come up in well under a minute, where the - /// plain image spends several minutes creating FREEPDB1 on first start. The tag has to name the major - /// version: the module reads it to decide that this image serves FREEPDB1 rather than XEPDB1. - /// - private const String Image = "gvenzl/oracle-free:23-slim-faststart"; - - private const String ServiceName = "FREEPDB1"; - - private const String SystemUsername = "SYSTEM"; + new OracleBuilder(Image).WithPassword(TestDatabaseContainers.Password); } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/PostgreSqlContainerFixture.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/PostgreSqlContainerFixture.cs index d8017a7..4d8cdf2 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/PostgreSqlContainerFixture.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/PostgreSqlContainerFixture.cs @@ -12,26 +12,25 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase.Containe /// Runs the PostgreSQL server the PostgreSQL integration tests use in a Docker container. /// internal sealed class PostgreSqlContainerFixture() - : DbContainerFixture(TestDatabaseDiagnosticMessageSink.Instance), ITestDatabaseContainerFixture + : DbContainerFixture(TestDatabaseDiagnosticMessageSink.Instance), + ITestDatabaseContainerFixture { + private const string Image = "postgres:latest"; + /// - public override String ConnectionString => + public override string ConnectionString => new NpgsqlConnectionStringBuilder { Host = this.Container.Hostname, Port = this.Container.GetMappedPublicPort(PostgreSqlBuilder.PostgreSqlPort), Username = PostgreSqlBuilder.DefaultUsername, - Password = TestDatabaseContainers.Password + Password = TestDatabaseContainers.Password, }.ConnectionString; /// - public override DbProviderFactory DbProviderFactory => - NpgsqlFactory.Instance; + public override DbProviderFactory DbProviderFactory => NpgsqlFactory.Instance; /// protected override PostgreSqlBuilder Configure() => - new PostgreSqlBuilder(Image) - .WithPassword(TestDatabaseContainers.Password); - - private const String Image = "postgres:latest"; + new PostgreSqlBuilder(Image).WithPassword(TestDatabaseContainers.Password); } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/SqlServerContainerFixture.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/SqlServerContainerFixture.cs index 6fbf037..880ef2a 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/SqlServerContainerFixture.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/SqlServerContainerFixture.cs @@ -11,10 +11,13 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase.Containe /// Runs the SQL Server server the SQL Server integration tests use in a Docker container. /// internal sealed class SqlServerContainerFixture() - : DbContainerFixture(TestDatabaseDiagnosticMessageSink.Instance), ITestDatabaseContainerFixture + : DbContainerFixture(TestDatabaseDiagnosticMessageSink.Instance), + ITestDatabaseContainerFixture { + private const string Image = "mcr.microsoft.com/mssql/server:2022-latest"; + /// - public override String ConnectionString => + public override string ConnectionString => new SqlConnectionStringBuilder { DataSource = $"{this.Container.Hostname},{this.Container.GetMappedPublicPort(MsSqlBuilder.MsSqlPort)}", @@ -26,17 +29,13 @@ internal sealed class SqlServerContainerFixture() Encrypt = false, // Several tests execute a command while a data reader is still open. - MultipleActiveResultSets = true + MultipleActiveResultSets = true, }.ConnectionString; /// - public override DbProviderFactory DbProviderFactory => - SqlClientFactory.Instance; + public override DbProviderFactory DbProviderFactory => SqlClientFactory.Instance; /// protected override MsSqlBuilder Configure() => - new MsSqlBuilder(Image) - .WithPassword(TestDatabaseContainers.Password); - - private const String Image = "mcr.microsoft.com/mssql/server:2022-latest"; + new MsSqlBuilder(Image).WithPassword(TestDatabaseContainers.Password); } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainer.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainer.cs index 06a9481..9aa7126 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainer.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainer.cs @@ -16,9 +16,18 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase.Containe /// and SQL Server never starts the MySQL, Oracle or PostgreSQL containers. Sharing is what keeps it to one /// container per database system - every test class asks for the same instance. /// -internal sealed class TestDatabaseContainer(String databaseSystemName) +internal sealed class TestDatabaseContainer(string databaseSystemName) where TFixture : class, ITestDatabaseContainerFixture, new() { + /// + /// The started - or currently starting - fixture. + /// + /// + /// defaults to , so the + /// task - and with it the container - is created once, no matter how many test classes ask for it. + /// + private readonly Lazy> fixture = new(() => CreateAndStartAsync(databaseSystemName)); + /// /// The fixture that runs the database server. /// @@ -28,8 +37,8 @@ internal sealed class TestDatabaseContainer(String databaseSystemName) ? this.fixture.Value.GetAwaiter().GetResult() : throw new InvalidOperationException( $"The {databaseSystemName} container has not been started. Tests reach a database through " - + $"{nameof(IntegrationTestsBase<>)}, which starts the container it needs before the first test " - + "of a test class runs." + + $"{nameof(IntegrationTestsBase<>)}, which starts the container it needs before the first test " + + "of a test class runs." ); /// @@ -54,10 +63,9 @@ public async ValueTask DisposeAsync() /// Starts the container and waits until the database server inside it accepts connections. Does nothing if the /// container is already starting or started. /// - public ValueTask StartAsync() => - new(this.fixture.Value); + public ValueTask StartAsync() => new(this.fixture.Value); - private static async Task CreateAndStartAsync(String databaseSystemName) + private static async Task CreateAndStartAsync(string databaseSystemName) { TestContext.Current.SendDiagnosticMessage($"Starting the {databaseSystemName} container ..."); @@ -75,18 +83,9 @@ private static async Task CreateAndStartAsync(String databaseSystemNam TestContext.Current.SendDiagnosticMessage( $"The {databaseSystemName} container is ready after {elapsedSeconds} seconds and is using the " - + $"following connection string: {connectionString}" + + $"following connection string: {connectionString}" ); return fixture; } - - /// - /// The started - or currently starting - fixture. - /// - /// - /// defaults to , so the - /// task - and with it the container - is created once, no matter how many test classes ask for it. - /// - private readonly Lazy> fixture = new(() => CreateAndStartAsync(databaseSystemName)); } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainerCleanup.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainerCleanup.cs index cb921e4..5e999dd 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainerCleanup.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainerCleanup.cs @@ -14,10 +14,8 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase.Containe public sealed class TestDatabaseContainerCleanup : IAsyncLifetime { /// - public ValueTask DisposeAsync() => - TestDatabaseContainers.DisposeAsync(); + public ValueTask DisposeAsync() => TestDatabaseContainers.DisposeAsync(); /// - public ValueTask InitializeAsync() => - default; + public ValueTask InitializeAsync() => default; } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainers.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainers.cs index fc03274..d7657f6 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainers.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseContainers.cs @@ -17,31 +17,35 @@ internal static class TestDatabaseContainers /// /// The password of the administrative database user in every container. /// - public const String Password = "TestTest123!"; + public const string Password = "TestTest123!"; + + private static readonly TestDatabaseContainer mySql = new("MySQL"); + + private static readonly TestDatabaseContainer oracle = new("Oracle"); + + private static readonly TestDatabaseContainer postgreSql = new("PostgreSQL"); + + private static readonly TestDatabaseContainer sqlServer = new("SQL Server"); /// /// The container running the MySQL server. /// - public static MySqlContainerFixture MySql => - mySql.Fixture; + public static MySqlContainerFixture MySql => mySql.Fixture; /// /// The container running the Oracle server. /// - public static OracleContainerFixture Oracle => - oracle.Fixture; + public static OracleContainerFixture Oracle => oracle.Fixture; /// /// The container running the PostgreSQL server. /// - public static PostgreSqlContainerFixture PostgreSql => - postgreSql.Fixture; + public static PostgreSqlContainerFixture PostgreSql => postgreSql.Fixture; /// /// The container running the SQL Server server. /// - public static SqlServerContainerFixture SqlServer => - sqlServer.Fixture; + public static SqlServerContainerFixture SqlServer => sqlServer.Fixture; /// /// Stops and removes every container that was started during the test run. @@ -57,32 +61,20 @@ public static async ValueTask DisposeAsync() /// /// Starts the MySQL container and waits until it accepts connections. /// - public static ValueTask StartMySqlAsync() => - mySql.StartAsync(); + public static ValueTask StartMySqlAsync() => mySql.StartAsync(); /// /// Starts the Oracle container and waits until it accepts connections. /// - public static ValueTask StartOracleAsync() => - oracle.StartAsync(); + public static ValueTask StartOracleAsync() => oracle.StartAsync(); /// /// Starts the PostgreSQL container and waits until it accepts connections. /// - public static ValueTask StartPostgreSqlAsync() => - postgreSql.StartAsync(); + public static ValueTask StartPostgreSqlAsync() => postgreSql.StartAsync(); /// /// Starts the SQL Server container and waits until it accepts connections. /// - public static ValueTask StartSqlServerAsync() => - sqlServer.StartAsync(); - - private static readonly TestDatabaseContainer mySql = new("MySQL"); - - private static readonly TestDatabaseContainer oracle = new("Oracle"); - - private static readonly TestDatabaseContainer postgreSql = new("PostgreSQL"); - - private static readonly TestDatabaseContainer sqlServer = new("SQL Server"); + public static ValueTask StartSqlServerAsync() => sqlServer.StartAsync(); } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseDiagnosticMessageSink.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseDiagnosticMessageSink.cs index 9101483..be4a02e 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseDiagnosticMessageSink.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/Containers/TestDatabaseDiagnosticMessageSink.cs @@ -26,7 +26,7 @@ internal sealed class TestDatabaseDiagnosticMessageSink : IMessageSink public static readonly TestDatabaseDiagnosticMessageSink Instance = new(); /// - public Boolean OnMessage(IMessageSinkMessage message) + public bool OnMessage(IMessageSinkMessage message) { if (message is IDiagnosticMessage diagnosticMessage) { diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/ITestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/ITestDatabaseProvider.cs index e72537a..3562748 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/ITestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/ITestDatabaseProvider.cs @@ -11,7 +11,7 @@ public interface ITestDatabaseProvider /// /// Determines whether the structure of temporary tables can be retrieved from the test database system. /// - public Boolean CanRetrieveStructureOfTemporaryTables { get; } + public bool CanRetrieveStructureOfTemporaryTables { get; } /// /// The database adapter for the test database. @@ -21,50 +21,61 @@ public interface ITestDatabaseProvider /// /// The collation of the test database. /// - public String DatabaseCollation { get; } + public string DatabaseCollation { get; } /// /// An SQL statement that delays query execution for two seconds. /// - public String DelayTwoSecondsStatement { get; } + public string DelayTwoSecondsStatement { get; } /// /// Determines whether the test database system has data types not supported by DbConnectionPlus. /// - public Boolean HasUnsupportedDataType { get; } + public bool HasUnsupportedDataType { get; } /// /// Determines whether the test database system supports executing commands while a data reader is open. /// - public Boolean SupportsCommandExecutionWhileDataReaderIsOpen { get; } + public bool SupportsCommandExecutionWhileDataReaderIsOpen { get; } /// /// Determines whether the test database system has a data type for the type . /// - public Boolean SupportsDateTimeOffset { get; } + public bool SupportsDateTimeOffset { get; } /// /// Determines whether the test database system supports proper command cancellation, meaning that cancelling a /// command (via ) actually stops its execution in the database and an appropriate /// exception is thrown. /// - public Boolean SupportsProperCommandCancellation { get; } + public bool SupportsProperCommandCancellation { get; } /// /// Determines whether the test database system supports stored procedures. /// - public Boolean SupportsStoredProcedures { get; } + public bool SupportsStoredProcedures { get; } /// /// Determines whether the test database system supports stored procedures which can return a result set. /// - public Boolean SupportsStoredProceduresReturningResultSet { get; } + public bool SupportsStoredProceduresReturningResultSet { get; } /// /// Determines whether a text column of a temporary table in the test database system inherits the collation /// from the current database. /// - public Boolean TemporaryTableTextColumnInheritsCollationFromDatabase { get; } + public bool TemporaryTableTextColumnInheritsCollationFromDatabase { get; } + + /// + /// Starts the database server the test database runs on and waits until it accepts connections. + /// + /// + /// Called by before the first test of a test class + /// runs, so implementations must do nothing when the server is already running. For a database system that + /// runs in a Docker container this starts the container - see + /// ; SQLite runs in-process and has nothing to start. + /// + public static abstract ValueTask StartDatabaseAsync(); /// /// Creates a connection to the test database. @@ -81,7 +92,7 @@ public interface ITestDatabaseProvider /// if a temporary table with the specified name exists in the test database; /// otherwise, . /// - public Boolean ExistsTemporaryTable(String tableName, DbConnection connection, DbTransaction? transaction = null); + public bool ExistsTemporaryTable(string tableName, DbConnection connection, DbTransaction? transaction = null); /// /// Gets the collation of the specified column in the specified temporary table. @@ -90,9 +101,9 @@ public interface ITestDatabaseProvider /// The name of the column whose collation to retrieve. /// The connection to the test database. /// The collation of the specified column in the specified temporary table. - public String GetCollationOfTemporaryTableColumn( - String temporaryTableName, - String columnName, + public string GetCollationOfTemporaryTableColumn( + string temporaryTableName, + string columnName, DbConnection connection ); @@ -105,9 +116,9 @@ DbConnection connection /// /// The data type of the specified column in the specified temporary table. /// - public String GetDataTypeOfTemporaryTableColumn( - String temporaryTableName, - String columnName, + public string GetDataTypeOfTemporaryTableColumn( + string temporaryTableName, + string columnName, DbConnection connection ); @@ -117,21 +128,10 @@ DbConnection connection /// /// A literal representing a data type in the test database system that is not supported by DbConnectionPlus. /// - public String GetUnsupportedDataTypeLiteral(); + public string GetUnsupportedDataTypeLiteral(); /// /// Prepares the test database and resets it to a clean state. /// public void ResetDatabase(); - - /// - /// Starts the database server the test database runs on and waits until it accepts connections. - /// - /// - /// Called by before the first test of a test class - /// runs, so implementations must do nothing when the server is already running. For a database system that - /// runs in a Docker container this starts the container - see - /// ; SQLite runs in-process and has nothing to start. - /// - public static abstract ValueTask StartDatabaseAsync(); } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs index e730661..166cf07 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/MySqlTestDatabaseProvider.cs @@ -11,143 +11,7 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; /// public class MySqlTestDatabaseProvider : ITestDatabaseProvider { - /// - public Boolean CanRetrieveStructureOfTemporaryTables => true; - - /// - public IDatabaseAdapter DatabaseAdapter => new MySqlDatabaseAdapter(); - - /// - public String DatabaseCollation => throw new NotImplementedException(); - - /// - public String DelayTwoSecondsStatement => "SELECT SLEEP(2);"; - - /// - public Boolean HasUnsupportedDataType => false; - - /// - public Boolean SupportsCommandExecutionWhileDataReaderIsOpen => false; - - /// - public Boolean SupportsDateTimeOffset => false; - - /// - public Boolean SupportsProperCommandCancellation => false; - - /// - public Boolean SupportsStoredProcedures => true; - - /// - public Boolean SupportsStoredProceduresReturningResultSet => true; - - /// - public Boolean TemporaryTableTextColumnInheritsCollationFromDatabase => true; - - /// - public DbConnection CreateConnection() - { - var connection = new MySqlConnection(ConnectionString); - connection.Open(); - - // Needed for MySqlBulkCopy to work. - connection.ExecuteNonQuery("SET GLOBAL local_infile=1"); - - connection.ChangeDatabase(DatabaseName); - - return connection; - } - - /// - public Boolean ExistsTemporaryTable(String tableName, DbConnection connection, DbTransaction? transaction = null) - { - try - { - // Only way to check for temporary table existence in MySQL is to try to query it. - connection.ExecuteNonQuery( - $"SELECT * FROM `{tableName}`", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ); - return true; - } - catch - { -#pragma warning disable ERP022 - return false; -#pragma warning restore ERP022 - } - } - - /// - public String GetCollationOfTemporaryTableColumn( - String temporaryTableName, - String columnName, - DbConnection connection - ) => - throw new NotImplementedException(); - - /// - public String GetDataTypeOfTemporaryTableColumn( - String temporaryTableName, - String columnName, - DbConnection connection - ) => - connection.Query<(String Field, String Type, String Null, String Key, Object Default, Object Extra)>( - $"SHOW COLUMNS FROM `{temporaryTableName}` WHERE Field = '{columnName}'", - cancellationToken: TestContext.Current.CancellationToken - ).Select(a => a.Type.ToUpper()).First(); - - /// - public String GetUnsupportedDataTypeLiteral() => - throw new NotImplementedException(); - - /// - public void ResetDatabase() - { - using var connection = new MySqlConnection(ConnectionString); - connection.Open(); - - if (!isDatabasePrepared) - { - connection.ExecuteNonQuery($"DROP DATABASE IF EXISTS `{DatabaseName}`"); - connection.ExecuteNonQuery($"CREATE DATABASE `{DatabaseName}`"); - - connection.ChangeDatabase(DatabaseName); - - ExecuteScript(connection, CreateDatabaseObjectsSql); - - isDatabasePrepared = true; - } - - connection.ChangeDatabase(DatabaseName); - ExecuteScript(connection, PurgeTablesSql); - } - - /// - public static ValueTask StartDatabaseAsync() => - TestDatabaseContainers.StartMySqlAsync(); - - /// - /// The connection string that connects to the MySQL server running in the test container. - /// - private static String ConnectionString => - TestDatabaseContainers.MySql.ConnectionString; - - private static void ExecuteScript(MySqlConnection connection, String script) - { - var statements = script - .Split("GO", StringSplitOptions.RemoveEmptyEntries) - .Where(a => !String.IsNullOrWhiteSpace(a.Trim())); - - foreach (var statement in statements) - { - connection.ExecuteNonQuery(statement); - } - } - - private const String CreateDatabaseObjectsSql = - """ + private const string CreateDatabaseObjectsSql = """ CREATE TABLE `Entity` ( `Id` BIGINT, @@ -252,10 +116,9 @@ FOR EACH ROW GO """; - private const String DatabaseName = "DbConnectionPlusTests"; + private const string DatabaseName = "DbConnectionPlusTests"; - private const String PurgeTablesSql = - """ + private const string PurgeTablesSql = """ TRUNCATE TABLE `Entity`; GO @@ -269,5 +132,139 @@ FOR EACH ROW GO """; - private static Boolean isDatabasePrepared; + private static bool isDatabasePrepared; + + /// + public bool CanRetrieveStructureOfTemporaryTables => true; + + /// + public IDatabaseAdapter DatabaseAdapter => new MySqlDatabaseAdapter(); + + /// + public string DatabaseCollation => throw new NotImplementedException(); + + /// + public string DelayTwoSecondsStatement => "SELECT SLEEP(2);"; + + /// + public bool HasUnsupportedDataType => false; + + /// + public bool SupportsCommandExecutionWhileDataReaderIsOpen => false; + + /// + public bool SupportsDateTimeOffset => false; + + /// + public bool SupportsProperCommandCancellation => false; + + /// + public bool SupportsStoredProcedures => true; + + /// + public bool SupportsStoredProceduresReturningResultSet => true; + + /// + public bool TemporaryTableTextColumnInheritsCollationFromDatabase => true; + + /// + /// The connection string that connects to the MySQL server running in the test container. + /// + private static string ConnectionString => TestDatabaseContainers.MySql.ConnectionString; + + /// + public static ValueTask StartDatabaseAsync() => TestDatabaseContainers.StartMySqlAsync(); + + /// + public DbConnection CreateConnection() + { + var connection = new MySqlConnection(ConnectionString); + connection.Open(); + + // Needed for MySqlBulkCopy to work. + connection.ExecuteNonQuery("SET GLOBAL local_infile=1"); + + connection.ChangeDatabase(DatabaseName); + + return connection; + } + + /// + public bool ExistsTemporaryTable(string tableName, DbConnection connection, DbTransaction? transaction = null) + { + try + { + // Only way to check for temporary table existence in MySQL is to try to query it. + connection.ExecuteNonQuery( + $"SELECT * FROM `{tableName}`", + transaction, + cancellationToken: TestContext.Current.CancellationToken + ); + return true; + } + catch + { +#pragma warning disable ERP022 + return false; +#pragma warning restore ERP022 + } + } + + /// + public string GetCollationOfTemporaryTableColumn( + string temporaryTableName, + string columnName, + DbConnection connection + ) => throw new NotImplementedException(); + + /// + public string GetDataTypeOfTemporaryTableColumn( + string temporaryTableName, + string columnName, + DbConnection connection + ) => + connection + .Query<(string Field, string Type, string Null, string Key, object Default, object Extra)>( + $"SHOW COLUMNS FROM `{temporaryTableName}` WHERE Field = '{columnName}'", + cancellationToken: TestContext.Current.CancellationToken + ) + .Select(a => a.Type.ToUpper()) + .First(); + + /// + public string GetUnsupportedDataTypeLiteral() => throw new NotImplementedException(); + + /// + public void ResetDatabase() + { + using var connection = new MySqlConnection(ConnectionString); + connection.Open(); + + if (!isDatabasePrepared) + { + connection.ExecuteNonQuery($"DROP DATABASE IF EXISTS `{DatabaseName}`"); + connection.ExecuteNonQuery($"CREATE DATABASE `{DatabaseName}`"); + + connection.ChangeDatabase(DatabaseName); + + ExecuteScript(connection, CreateDatabaseObjectsSql); + + isDatabasePrepared = true; + } + + connection.ChangeDatabase(DatabaseName); + ExecuteScript(connection, PurgeTablesSql); + } + + private static void ExecuteScript(MySqlConnection connection, string script) + { + var statements = script + .Split("GO", StringSplitOptions.RemoveEmptyEntries) + .Where(a => !string.IsNullOrWhiteSpace(a.Trim())); + + foreach (var statement in statements) + { + connection.ExecuteNonQuery(statement); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs index 157d683..e9abf61 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/OracleTestDatabaseProvider.cs @@ -11,124 +11,7 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; /// public class OracleTestDatabaseProvider : ITestDatabaseProvider { - /// - public Boolean CanRetrieveStructureOfTemporaryTables => false; - - /// - public IDatabaseAdapter DatabaseAdapter => new OracleDatabaseAdapter(); - - /// - public String DatabaseCollation => throw new NotImplementedException(); - - /// - public String DelayTwoSecondsStatement => "BEGIN DBMS_LOCK.SLEEP(2); END;"; - - /// - public Boolean HasUnsupportedDataType => false; - - /// - public Boolean SupportsCommandExecutionWhileDataReaderIsOpen => true; - - /// - public Boolean SupportsDateTimeOffset => true; - - /// - public Boolean SupportsProperCommandCancellation => false; - - /// - public Boolean SupportsStoredProcedures => true; - - /// - public Boolean SupportsStoredProceduresReturningResultSet => false; - - /// - public Boolean TemporaryTableTextColumnInheritsCollationFromDatabase => true; - - /// - public DbConnection CreateConnection() - { - var connection = new OracleConnection(ConnectionString); - - // Clear the connection we got from the pool, so that its session actually ends. - // Otherwise, Oracle will keep temporary tables alive for that session and we will eventually run out of them. - OracleConnection.ClearPool(connection); - - connection.Open(); - return connection; - } - - /// - public Boolean ExistsTemporaryTable(String tableName, DbConnection connection, DbTransaction? transaction = null) - { - var quoteTemporaryTableName = this.DatabaseAdapter.QuoteTemporaryTableName(tableName, connection); - var unquotedTemporaryTableName = quoteTemporaryTableName[1..^1]; // Strip the quotes ("). - - return connection.Exists( - $"SELECT * FROM USER_PRIVATE_TEMP_TABLES WHERE TABLE_NAME = {Parameter(unquotedTemporaryTableName)}" - ); - } - - /// - public String GetCollationOfTemporaryTableColumn( - String temporaryTableName, - String columnName, - DbConnection connection - ) => - throw new NotImplementedException(); - - /// - public String GetDataTypeOfTemporaryTableColumn( - String temporaryTableName, - String columnName, - DbConnection connection - ) => - throw new NotImplementedException(); - - /// - public String GetUnsupportedDataTypeLiteral() => - throw new NotImplementedException(); - - /// - public void ResetDatabase() - { - using var connection = new OracleConnection(ConnectionString); - connection.Open(); - - if (!isDatabasePrepared) - { - ExecuteScript(connection, DropDatabaseObjectsSql); - ExecuteScript(connection, CreateDatabaseObjectsSql); - - isDatabasePrepared = true; - } - - ExecuteScript(connection, PurgeTablesSql); - } - - /// - public static ValueTask StartDatabaseAsync() => - TestDatabaseContainers.StartOracleAsync(); - - /// - /// The connection string that connects to the Oracle server running in the test container. - /// - private static String ConnectionString => - TestDatabaseContainers.Oracle.ConnectionString; - - private static void ExecuteScript(OracleConnection connection, String script) - { - var statements = script - .Split("GO", StringSplitOptions.RemoveEmptyEntries) - .Where(a => !String.IsNullOrWhiteSpace(a.Trim())); - - foreach (var statement in statements) - { - connection.ExecuteNonQuery(statement); - } - } - - private const String CreateDatabaseObjectsSql = - """ + private const string CreateDatabaseObjectsSql = """ CREATE TABLE "Entity" ( "Id" NUMBER(19) NOT NULL PRIMARY KEY, @@ -204,8 +87,7 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS """; - private const String DropDatabaseObjectsSql = - """ + private const string DropDatabaseObjectsSql = """ DROP TABLE IF EXISTS "Entity" PURGE; GO @@ -225,8 +107,7 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS GO """; - private const String PurgeTablesSql = - """ + private const string PurgeTablesSql = """ TRUNCATE TABLE "Entity"; GO @@ -243,5 +124,116 @@ CREATE OR REPLACE NONEDITIONABLE PROCEDURE "DeleteAllEntities" AS GO """; - private static Boolean isDatabasePrepared; + private static bool isDatabasePrepared; + + /// + public bool CanRetrieveStructureOfTemporaryTables => false; + + /// + public IDatabaseAdapter DatabaseAdapter => new OracleDatabaseAdapter(); + + /// + public string DatabaseCollation => throw new NotImplementedException(); + + /// + public string DelayTwoSecondsStatement => "BEGIN DBMS_LOCK.SLEEP(2); END;"; + + /// + public bool HasUnsupportedDataType => false; + + /// + public bool SupportsCommandExecutionWhileDataReaderIsOpen => true; + + /// + public bool SupportsDateTimeOffset => true; + + /// + public bool SupportsProperCommandCancellation => false; + + /// + public bool SupportsStoredProcedures => true; + + /// + public bool SupportsStoredProceduresReturningResultSet => false; + + /// + public bool TemporaryTableTextColumnInheritsCollationFromDatabase => true; + + /// + /// The connection string that connects to the Oracle server running in the test container. + /// + private static string ConnectionString => TestDatabaseContainers.Oracle.ConnectionString; + + /// + public static ValueTask StartDatabaseAsync() => TestDatabaseContainers.StartOracleAsync(); + + /// + public DbConnection CreateConnection() + { + var connection = new OracleConnection(ConnectionString); + + // Clear the connection we got from the pool, so that its session actually ends. + // Otherwise, Oracle will keep temporary tables alive for that session and we will eventually run out of them. + OracleConnection.ClearPool(connection); + + connection.Open(); + return connection; + } + + /// + public bool ExistsTemporaryTable(string tableName, DbConnection connection, DbTransaction? transaction = null) + { + var quoteTemporaryTableName = this.DatabaseAdapter.QuoteTemporaryTableName(tableName, connection); + var unquotedTemporaryTableName = quoteTemporaryTableName[1..^1]; // Strip the quotes ("). + + return connection.Exists( + $"SELECT * FROM USER_PRIVATE_TEMP_TABLES WHERE TABLE_NAME = {Parameter(unquotedTemporaryTableName)}" + ); + } + + /// + public string GetCollationOfTemporaryTableColumn( + string temporaryTableName, + string columnName, + DbConnection connection + ) => throw new NotImplementedException(); + + /// + public string GetDataTypeOfTemporaryTableColumn( + string temporaryTableName, + string columnName, + DbConnection connection + ) => throw new NotImplementedException(); + + /// + public string GetUnsupportedDataTypeLiteral() => throw new NotImplementedException(); + + /// + public void ResetDatabase() + { + using var connection = new OracleConnection(ConnectionString); + connection.Open(); + + if (!isDatabasePrepared) + { + ExecuteScript(connection, DropDatabaseObjectsSql); + ExecuteScript(connection, CreateDatabaseObjectsSql); + + isDatabasePrepared = true; + } + + ExecuteScript(connection, PurgeTablesSql); + } + + private static void ExecuteScript(OracleConnection connection, string script) + { + var statements = script + .Split("GO", StringSplitOptions.RemoveEmptyEntries) + .Where(a => !string.IsNullOrWhiteSpace(a.Trim())); + + foreach (var statement in statements) + { + connection.ExecuteNonQuery(statement); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs index 684b6f4..672930f 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/PostgreSqlTestDatabaseProvider.cs @@ -11,123 +11,7 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; /// public class PostgreSqlTestDatabaseProvider : ITestDatabaseProvider { - /// - public Boolean CanRetrieveStructureOfTemporaryTables => true; - - /// - public IDatabaseAdapter DatabaseAdapter => new PostgreSqlDatabaseAdapter(); - - /// - public String DatabaseCollation => throw new NotImplementedException(); - - /// - public String DelayTwoSecondsStatement => "SELECT pg_sleep(2);"; - - /// - public Boolean HasUnsupportedDataType => true; - - /// - public Boolean SupportsCommandExecutionWhileDataReaderIsOpen => false; - - /// - public Boolean SupportsDateTimeOffset => false; - - /// - public Boolean SupportsProperCommandCancellation => true; - - /// - public Boolean SupportsStoredProcedures => true; - - /// - public Boolean SupportsStoredProceduresReturningResultSet => false; - - /// - public Boolean TemporaryTableTextColumnInheritsCollationFromDatabase => true; - - /// - public DbConnection CreateConnection() - { - var connection = new NpgsqlConnection(ConnectionString); - connection.Open(); - connection.ChangeDatabase(DatabaseName); - return connection; - } - - /// - public Boolean ExistsTemporaryTable(String tableName, DbConnection connection, DbTransaction? transaction = null) => - connection.Exists( - $""" - SELECT 1 - FROM information_schema.tables - WHERE table_type = 'LOCAL TEMPORARY' AND - table_name = '{tableName}' - """, - transaction, - cancellationToken: TestContext.Current.CancellationToken - ); - - /// - public String GetCollationOfTemporaryTableColumn( - String temporaryTableName, - String columnName, - DbConnection connection - ) => - throw new NotImplementedException(); - - /// - public String GetDataTypeOfTemporaryTableColumn( - String temporaryTableName, - String columnName, - DbConnection connection - ) => - connection.QuerySingle( - $""" - SELECT data_type - FROM information_schema.columns - WHERE table_schema LIKE 'pg_temp%' AND - table_name = '{temporaryTableName}' AND - column_name = '{columnName}' - """, - cancellationToken: TestContext.Current.CancellationToken - ); - - /// - public String GetUnsupportedDataTypeLiteral() => - "(1, 2)"; - - public void ResetDatabase() - { - using var connection = new NpgsqlConnection(ConnectionString); - connection.Open(); - - if (!isDatabasePrepared) - { - connection.ExecuteNonQuery($"DROP DATABASE IF EXISTS \"{DatabaseName}\" WITH (FORCE)"); - connection.ExecuteNonQuery($"CREATE DATABASE \"{DatabaseName}\""); - - connection.ChangeDatabase(DatabaseName); - - connection.ExecuteNonQuery(CreateDatabaseObjectsSql); - - isDatabasePrepared = true; - } - - connection.ChangeDatabase(DatabaseName); - connection.ExecuteNonQuery(PurgeTablesSql); - } - - /// - public static ValueTask StartDatabaseAsync() => - TestDatabaseContainers.StartPostgreSqlAsync(); - - /// - /// The connection string that connects to the PostgreSQL server running in the test container. - /// - private static String ConnectionString => - TestDatabaseContainers.PostgreSql.ConnectionString; - - private const String CreateDatabaseObjectsSql = - """ + private const string CreateDatabaseObjectsSql = """ CREATE EXTENSION IF NOT EXISTS pgcrypto; -- Needed for gen_random_bytes() CREATE TABLE "Entity" @@ -228,15 +112,125 @@ DELETE FROM "Entity" $$; """; - private const String DatabaseName = "DbConnectionPlusTests"; + private const string DatabaseName = "DbConnectionPlusTests"; - private const String PurgeTablesSql = - """ + private const string PurgeTablesSql = """ TRUNCATE TABLE "Entity"; TRUNCATE TABLE "EntityWithEnumStoredAsString"; TRUNCATE TABLE "EntityWithEnumStoredAsInteger"; TRUNCATE TABLE "MappingTestEntity"; """; - private static Boolean isDatabasePrepared; + private static bool isDatabasePrepared; + + /// + public bool CanRetrieveStructureOfTemporaryTables => true; + + /// + public IDatabaseAdapter DatabaseAdapter => new PostgreSqlDatabaseAdapter(); + + /// + public string DatabaseCollation => throw new NotImplementedException(); + + /// + public string DelayTwoSecondsStatement => "SELECT pg_sleep(2);"; + + /// + public bool HasUnsupportedDataType => true; + + /// + public bool SupportsCommandExecutionWhileDataReaderIsOpen => false; + + /// + public bool SupportsDateTimeOffset => false; + + /// + public bool SupportsProperCommandCancellation => true; + + /// + public bool SupportsStoredProcedures => true; + + /// + public bool SupportsStoredProceduresReturningResultSet => false; + + /// + public bool TemporaryTableTextColumnInheritsCollationFromDatabase => true; + + /// + /// The connection string that connects to the PostgreSQL server running in the test container. + /// + private static string ConnectionString => TestDatabaseContainers.PostgreSql.ConnectionString; + + /// + public static ValueTask StartDatabaseAsync() => TestDatabaseContainers.StartPostgreSqlAsync(); + + /// + public DbConnection CreateConnection() + { + var connection = new NpgsqlConnection(ConnectionString); + connection.Open(); + connection.ChangeDatabase(DatabaseName); + return connection; + } + + /// + public bool ExistsTemporaryTable(string tableName, DbConnection connection, DbTransaction? transaction = null) => + connection.Exists( + $""" + SELECT 1 + FROM information_schema.tables + WHERE table_type = 'LOCAL TEMPORARY' AND + table_name = '{tableName}' + """, + transaction, + cancellationToken: TestContext.Current.CancellationToken + ); + + /// + public string GetCollationOfTemporaryTableColumn( + string temporaryTableName, + string columnName, + DbConnection connection + ) => throw new NotImplementedException(); + + /// + public string GetDataTypeOfTemporaryTableColumn( + string temporaryTableName, + string columnName, + DbConnection connection + ) => + connection.QuerySingle( + $""" + SELECT data_type + FROM information_schema.columns + WHERE table_schema LIKE 'pg_temp%' AND + table_name = '{temporaryTableName}' AND + column_name = '{columnName}' + """, + cancellationToken: TestContext.Current.CancellationToken + ); + + /// + public string GetUnsupportedDataTypeLiteral() => "(1, 2)"; + + public void ResetDatabase() + { + using var connection = new NpgsqlConnection(ConnectionString); + connection.Open(); + + if (!isDatabasePrepared) + { + connection.ExecuteNonQuery($"DROP DATABASE IF EXISTS \"{DatabaseName}\" WITH (FORCE)"); + connection.ExecuteNonQuery($"CREATE DATABASE \"{DatabaseName}\""); + + connection.ChangeDatabase(DatabaseName); + + connection.ExecuteNonQuery(CreateDatabaseObjectsSql); + + isDatabasePrepared = true; + } + + connection.ChangeDatabase(DatabaseName); + connection.ExecuteNonQuery(PurgeTablesSql); + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs index 0efe85b..af136bc 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SQLiteTestDatabaseProvider.cs @@ -12,6 +12,72 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; /// public class SqliteTestDatabaseProvider : ITestDatabaseProvider { + private const string CreateDatabaseObjectsSql = """ + CREATE TABLE Entity + ( + Id INTEGER, + BooleanValue INTEGER, + BytesValue BLOB, + ByteValue INTEGER, + CharValue TEXT, + DateOnlyValue TEXT, + DateTimeValue TEXT, + DecimalValue TEXT, + DoubleValue REAL, + EnumValue TEXT, + GuidValue TEXT, + Int16Value INTEGER, + Int32Value INTEGER, + Int64Value INTEGER, + NullableBooleanValue INTEGER NULL, + SingleValue REAL, + StringValue TEXT, + TimeOnlyValue TEXT, + TimeSpanValue TEXT + ); + + CREATE TABLE EntityWithDateTimeOffset + ( + Id INTEGER, + DateTimeOffsetValue TEXT + ); + + CREATE TABLE EntityWithEnumStoredAsString + ( + Id INTEGER, + Enum TEXT + ); + + CREATE TABLE EntityWithEnumStoredAsInteger + ( + Id INTEGER, + Enum INTEGER + ); + + CREATE TABLE MappingTestEntity + ( + Computed INTEGER GENERATED ALWAYS AS (Value+999) VIRTUAL, + ConcurrencyToken BLOB, + Identity INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + Key1 INTEGER NOT NULL, + Key2 INTEGER NOT NULL, + Value INTEGER NOT NULL, + RowVersion BLOB DEFAULT (randomblob(8)), + NotMapped TEXT NULL + ); + + CREATE TRIGGER TriggerMappingTestEntity + BEFORE UPDATE ON MappingTestEntity + FOR EACH ROW + BEGIN + UPDATE MappingTestEntity SET RowVersion = randomblob(8) WHERE Key1 = OLD.Key1 AND Key2 = OLD.Key2; + END; + """; + + private readonly SqliteConnection connection; + + private bool isDatabasePrepared; + /// /// Initializes a new instance of the class. /// @@ -22,82 +88,84 @@ public SqliteTestDatabaseProvider() } /// - public Boolean CanRetrieveStructureOfTemporaryTables => true; + public bool CanRetrieveStructureOfTemporaryTables => true; /// public IDatabaseAdapter DatabaseAdapter => new SqliteDatabaseAdapter(); /// - public String DatabaseCollation => throw new NotImplementedException(); + public string DatabaseCollation => throw new NotImplementedException(); /// - public String DelayTwoSecondsStatement => + public string DelayTwoSecondsStatement => """ - WITH RECURSIVE delay(x) AS ( - SELECT 1 - UNION ALL - SELECT x + 1 FROM delay WHERE x < 5000000 - ) - SELECT x FROM delay WHERE x = 5000000; - """; + WITH RECURSIVE delay(x) AS ( + SELECT 1 + UNION ALL + SELECT x + 1 FROM delay WHERE x < 5000000 + ) + SELECT x FROM delay WHERE x = 5000000; + """; + + /// + public bool HasUnsupportedDataType => false; /// - public Boolean HasUnsupportedDataType => false; + public bool SupportsCommandExecutionWhileDataReaderIsOpen => true; /// - public Boolean SupportsCommandExecutionWhileDataReaderIsOpen => true; + public bool SupportsDateTimeOffset => true; /// - public Boolean SupportsDateTimeOffset => true; + public bool SupportsProperCommandCancellation => false; /// - public Boolean SupportsProperCommandCancellation => false; + public bool SupportsStoredProcedures => false; /// - public Boolean SupportsStoredProcedures => false; + public bool SupportsStoredProceduresReturningResultSet => false; /// - public Boolean SupportsStoredProceduresReturningResultSet => false; + public bool TemporaryTableTextColumnInheritsCollationFromDatabase => true; /// - public Boolean TemporaryTableTextColumnInheritsCollationFromDatabase => true; + /// SQLite runs in-process, in memory, so there is no server and nothing to start. + public static ValueTask StartDatabaseAsync() => default; /// - public DbConnection CreateConnection() => - this.connection; + public DbConnection CreateConnection() => this.connection; /// - public Boolean ExistsTemporaryTable(String tableName, DbConnection connection, DbTransaction? transaction = null) => + public bool ExistsTemporaryTable(string tableName, DbConnection connection, DbTransaction? transaction = null) => this.connection.Exists( $""" - SELECT 1 - FROM sqlite_temp_master - WHERE type = 'table' - AND name = '{tableName}' - """, + SELECT 1 + FROM sqlite_temp_master + WHERE type = 'table' + AND name = '{tableName}' + """, transaction, cancellationToken: TestContext.Current.CancellationToken ); /// - public String GetCollationOfTemporaryTableColumn( - String temporaryTableName, - String columnName, + public string GetCollationOfTemporaryTableColumn( + string temporaryTableName, + string columnName, DbConnection connection - ) => - throw new NotImplementedException(); + ) => throw new NotImplementedException(); /// - public String GetDataTypeOfTemporaryTableColumn( - String temporaryTableName, - String columnName, + public string GetDataTypeOfTemporaryTableColumn( + string temporaryTableName, + string columnName, DbConnection connection ) => - this.connection - .Query<(Int32 cid, String name, String Type, Boolean notnull, Object dflt_value, Int32 pk)>( + this + .connection.Query<(int cid, string name, string Type, bool notnull, object dflt_value, int pk)>( $""" - PRAGMA table_info("{temporaryTableName}"); - """, + PRAGMA table_info("{temporaryTableName}"); + """, cancellationToken: TestContext.Current.CancellationToken ) .Where(a => a.name == columnName) @@ -105,8 +173,7 @@ DbConnection connection .Single(); /// - public String GetUnsupportedDataTypeLiteral() => - throw new NotImplementedException(); + public string GetUnsupportedDataTypeLiteral() => throw new NotImplementedException(); /// public void ResetDatabase() @@ -118,76 +185,4 @@ public void ResetDatabase() this.isDatabasePrepared = true; } } - - /// - /// SQLite runs in-process, in memory, so there is no server and nothing to start. - public static ValueTask StartDatabaseAsync() => - default; - - private readonly SqliteConnection connection; - - private Boolean isDatabasePrepared; - - private const String CreateDatabaseObjectsSql = - """ - CREATE TABLE Entity - ( - Id INTEGER, - BooleanValue INTEGER, - BytesValue BLOB, - ByteValue INTEGER, - CharValue TEXT, - DateOnlyValue TEXT, - DateTimeValue TEXT, - DecimalValue TEXT, - DoubleValue REAL, - EnumValue TEXT, - GuidValue TEXT, - Int16Value INTEGER, - Int32Value INTEGER, - Int64Value INTEGER, - NullableBooleanValue INTEGER NULL, - SingleValue REAL, - StringValue TEXT, - TimeOnlyValue TEXT, - TimeSpanValue TEXT - ); - - CREATE TABLE EntityWithDateTimeOffset - ( - Id INTEGER, - DateTimeOffsetValue TEXT - ); - - CREATE TABLE EntityWithEnumStoredAsString - ( - Id INTEGER, - Enum TEXT - ); - - CREATE TABLE EntityWithEnumStoredAsInteger - ( - Id INTEGER, - Enum INTEGER - ); - - CREATE TABLE MappingTestEntity - ( - Computed INTEGER GENERATED ALWAYS AS (Value+999) VIRTUAL, - ConcurrencyToken BLOB, - Identity INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - Key1 INTEGER NOT NULL, - Key2 INTEGER NOT NULL, - Value INTEGER NOT NULL, - RowVersion BLOB DEFAULT (randomblob(8)), - NotMapped TEXT NULL - ); - - CREATE TRIGGER TriggerMappingTestEntity - BEFORE UPDATE ON MappingTestEntity - FOR EACH ROW - BEGIN - UPDATE MappingTestEntity SET RowVersion = randomblob(8) WHERE Key1 = OLD.Key1 AND Key2 = OLD.Key2; - END; - """; } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs index 299f67a..f2ace9c 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/SqlServerTestDatabaseProvider.cs @@ -10,149 +10,7 @@ namespace RentADeveloper.DbConnectionPlus.IntegrationTests.TestDatabase; /// public class SqlServerTestDatabaseProvider : ITestDatabaseProvider { - /// - public Boolean CanRetrieveStructureOfTemporaryTables => true; - - /// - public IDatabaseAdapter DatabaseAdapter => new SqlServerDatabaseAdapter(); - - /// - public String DatabaseCollation => "Latin1_General_CI_AS"; - - /// - public String DelayTwoSecondsStatement => "WAITFOR DELAY '00:00:02';"; - - /// - public Boolean HasUnsupportedDataType => true; - - /// - public Boolean SupportsCommandExecutionWhileDataReaderIsOpen => true; - - /// - public Boolean SupportsDateTimeOffset => true; - - /// - public Boolean SupportsProperCommandCancellation => true; - - /// - public Boolean SupportsStoredProcedures => true; - - /// - public Boolean SupportsStoredProceduresReturningResultSet => true; - - /// - public Boolean TemporaryTableTextColumnInheritsCollationFromDatabase => false; - - /// - public DbConnection CreateConnection() - { - var connection = new SqlConnection(ConnectionString); - connection.Open(); - - connection.ChangeDatabase(DatabaseName); - - return connection; - } - - /// - public Boolean ExistsTemporaryTable(String tableName, DbConnection connection, DbTransaction? transaction = null) => - connection.ExecuteScalar( - $"IF OBJECT_ID('tempdb..#{tableName}', 'U') IS NOT NULL SELECT 1 ELSE SELECT 0", - transaction, - cancellationToken: TestContext.Current.CancellationToken - ); - - /// - public String GetCollationOfTemporaryTableColumn( - String temporaryTableName, - String columnName, - DbConnection connection - ) => - connection.ExecuteScalar( - $""" - SELECT C.collation_name AS CollationName - FROM tempdb.sys.columns C - WHERE c.object_id = OBJECT_ID('tempdb..#{temporaryTableName}') AND C.name = '{columnName}' - """, - cancellationToken: TestContext.Current.CancellationToken - ); - - /// - public String GetDataTypeOfTemporaryTableColumn( - String temporaryTableName, - String columnName, - DbConnection connection - ) => - connection.QuerySingle( - $""" - SELECT t.name AS DataType - FROM tempdb.sys.columns c - JOIN tempdb.sys.types t ON c.user_type_id = t.user_type_id - WHERE c.object_id = OBJECT_ID('tempdb..#{temporaryTableName}') AND c.name = '{columnName}' - """, - cancellationToken: TestContext.Current.CancellationToken - ); - - /// - public String GetUnsupportedDataTypeLiteral() => - "CONVERT(SQL_VARIANT, 123)"; - - /// - public void ResetDatabase() - { - using var connection = new SqlConnection(ConnectionString); - connection.Open(); - - if (!isDatabasePrepared) - { - connection.ExecuteNonQuery( - $""" - IF EXISTS (SELECT name FROM sys.databases WHERE name = N'{DatabaseName}') - BEGIN - ALTER DATABASE [{DatabaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; - DROP DATABASE [{DatabaseName}]; - END - """ - ); - - connection.ExecuteNonQuery($"CREATE DATABASE [{DatabaseName}] COLLATE {this.DatabaseCollation}"); - - connection.ChangeDatabase(DatabaseName); - - ExecuteScript(connection, CreateDatabaseObjectsSql); - - isDatabasePrepared = true; - } - - connection.ChangeDatabase(DatabaseName); - - ExecuteScript(connection, PurgeTablesSql); - } - - /// - public static ValueTask StartDatabaseAsync() => - TestDatabaseContainers.StartSqlServerAsync(); - - /// - /// The connection string that connects to the SQL Server server running in the test container. - /// - private static String ConnectionString => - TestDatabaseContainers.SqlServer.ConnectionString; - - private static void ExecuteScript(SqlConnection connection, String script) - { - var statements = script - .Split("GO", StringSplitOptions.RemoveEmptyEntries) - .Where(a => !String.IsNullOrWhiteSpace(a.Trim())); - - foreach (var statement in statements) - { - connection.ExecuteNonQuery(statement); - } - } - - private const String CreateDatabaseObjectsSql = - """ + private const string CreateDatabaseObjectsSql = """ CREATE TABLE Entity ( Id BIGINT NOT NULL PRIMARY KEY, @@ -255,10 +113,9 @@ DELETE FROM Entity GO """; - private const String DatabaseName = "DbConnectionPlusTests"; + private const string DatabaseName = "DbConnectionPlusTests"; - private const String PurgeTablesSql = - """ + private const string PurgeTablesSql = """ TRUNCATE TABLE Entity; GO @@ -275,5 +132,143 @@ DELETE FROM Entity GO """; - private static Boolean isDatabasePrepared; + private static bool isDatabasePrepared; + + /// + public bool CanRetrieveStructureOfTemporaryTables => true; + + /// + public IDatabaseAdapter DatabaseAdapter => new SqlServerDatabaseAdapter(); + + /// + public string DatabaseCollation => "Latin1_General_CI_AS"; + + /// + public string DelayTwoSecondsStatement => "WAITFOR DELAY '00:00:02';"; + + /// + public bool HasUnsupportedDataType => true; + + /// + public bool SupportsCommandExecutionWhileDataReaderIsOpen => true; + + /// + public bool SupportsDateTimeOffset => true; + + /// + public bool SupportsProperCommandCancellation => true; + + /// + public bool SupportsStoredProcedures => true; + + /// + public bool SupportsStoredProceduresReturningResultSet => true; + + /// + public bool TemporaryTableTextColumnInheritsCollationFromDatabase => false; + + /// + /// The connection string that connects to the SQL Server server running in the test container. + /// + private static string ConnectionString => TestDatabaseContainers.SqlServer.ConnectionString; + + /// + public static ValueTask StartDatabaseAsync() => TestDatabaseContainers.StartSqlServerAsync(); + + /// + public DbConnection CreateConnection() + { + var connection = new SqlConnection(ConnectionString); + connection.Open(); + + connection.ChangeDatabase(DatabaseName); + + return connection; + } + + /// + public bool ExistsTemporaryTable(string tableName, DbConnection connection, DbTransaction? transaction = null) => + connection.ExecuteScalar( + $"IF OBJECT_ID('tempdb..#{tableName}', 'U') IS NOT NULL SELECT 1 ELSE SELECT 0", + transaction, + cancellationToken: TestContext.Current.CancellationToken + ); + + /// + public string GetCollationOfTemporaryTableColumn( + string temporaryTableName, + string columnName, + DbConnection connection + ) => + connection.ExecuteScalar( + $""" + SELECT C.collation_name AS CollationName + FROM tempdb.sys.columns C + WHERE c.object_id = OBJECT_ID('tempdb..#{temporaryTableName}') AND C.name = '{columnName}' + """, + cancellationToken: TestContext.Current.CancellationToken + ); + + /// + public string GetDataTypeOfTemporaryTableColumn( + string temporaryTableName, + string columnName, + DbConnection connection + ) => + connection.QuerySingle( + $""" + SELECT t.name AS DataType + FROM tempdb.sys.columns c + JOIN tempdb.sys.types t ON c.user_type_id = t.user_type_id + WHERE c.object_id = OBJECT_ID('tempdb..#{temporaryTableName}') AND c.name = '{columnName}' + """, + cancellationToken: TestContext.Current.CancellationToken + ); + + /// + public string GetUnsupportedDataTypeLiteral() => "CONVERT(SQL_VARIANT, 123)"; + + /// + public void ResetDatabase() + { + using var connection = new SqlConnection(ConnectionString); + connection.Open(); + + if (!isDatabasePrepared) + { + connection.ExecuteNonQuery( + $""" + IF EXISTS (SELECT name FROM sys.databases WHERE name = N'{DatabaseName}') + BEGIN + ALTER DATABASE [{DatabaseName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE [{DatabaseName}]; + END + """ + ); + + connection.ExecuteNonQuery($"CREATE DATABASE [{DatabaseName}] COLLATE {this.DatabaseCollation}"); + + connection.ChangeDatabase(DatabaseName); + + ExecuteScript(connection, CreateDatabaseObjectsSql); + + isDatabasePrepared = true; + } + + connection.ChangeDatabase(DatabaseName); + + ExecuteScript(connection, PurgeTablesSql); + } + + private static void ExecuteScript(SqlConnection connection, string script) + { + var statements = script + .Split("GO", StringSplitOptions.RemoveEmptyEntries) + .Where(a => !string.IsNullOrWhiteSpace(a.Trim())); + + foreach (var statement in statements) + { + connection.ExecuteNonQuery(statement); + } + } } diff --git a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/TestDatabaseFixture.cs b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/TestDatabaseFixture.cs index e90e677..2d76054 100644 --- a/tests/DbConnectionPlus.IntegrationTests/TestDatabase/TestDatabaseFixture.cs +++ b/tests/DbConnectionPlus.IntegrationTests/TestDatabase/TestDatabaseFixture.cs @@ -23,10 +23,8 @@ public sealed class TestDatabaseFixture : IAsyncLifetime where TTestDatabaseProvider : ITestDatabaseProvider, new() { /// - public ValueTask DisposeAsync() => - default; + public ValueTask DisposeAsync() => default; /// - public ValueTask InitializeAsync() => - TTestDatabaseProvider.StartDatabaseAsync(); + public ValueTask InitializeAsync() => TTestDatabaseProvider.StartDatabaseAsync(); } diff --git a/tests/DbConnectionPlus.UnitTests/Assertions/AssertionsExtensions.cs b/tests/DbConnectionPlus.UnitTests/Assertions/AssertionsExtensions.cs index 55301d7..5362115 100644 --- a/tests/DbConnectionPlus.UnitTests/Assertions/AssertionsExtensions.cs +++ b/tests/DbConnectionPlus.UnitTests/Assertions/AssertionsExtensions.cs @@ -16,7 +16,8 @@ public static class AssertionsExtensions /// The expected types of which the subject should be one. [CustomAssertion] public static void BeAnyOf(this TypeAssertions assertions, params Type[] expectations) => - AssertionChain.GetOrCreate() + AssertionChain + .GetOrCreate() .ForCondition(expectations.Contains(assertions.Subject)) .FailWith("Expected {context} to be any of {0}, but found {1}", expectations, assertions.Subject); } diff --git a/tests/DbConnectionPlus.UnitTests/Assertions/DecoratorAssertions.cs b/tests/DbConnectionPlus.UnitTests/Assertions/DecoratorAssertions.cs index 5fe303a..c916e93 100644 --- a/tests/DbConnectionPlus.UnitTests/Assertions/DecoratorAssertions.cs +++ b/tests/DbConnectionPlus.UnitTests/Assertions/DecoratorAssertions.cs @@ -11,6 +11,15 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Assertions; /// public static class DecoratorAssertions { + /// + /// The method. + /// + private static readonly MethodInfo specimenFactoryCreateMethod = typeof(SpecimenFactory).GetMethod( + nameof(SpecimenFactory.Create), + BindingFlags.Public | BindingFlags.Static, + [typeof(ISpecimenBuilder)] + )!; + /// /// /// Asserts that forwards all calls to , meaning each @@ -29,7 +38,7 @@ public static void AssertDecoratorForwardsAllCalls( Fixture fixture, TDecorator decorator, TDecorator decorated, - HashSet excludedMethods + HashSet excludedMethods ) where TDecorator : class { @@ -45,7 +54,7 @@ HashSet excludedMethods { var methodParameters = method.GetParameters(); - var decoratorMethodArguments = new Object?[methodParameters.Length]; + var decoratorMethodArguments = new object?[methodParameters.Length]; for (var i = 0; i < methodParameters.Length; i++) { @@ -64,7 +73,7 @@ HashSet excludedMethods } } - Object? decoratedMethodReturnValue = null; + object? decoratedMethodReturnValue = null; if (method.ReturnType != typeof(void)) { @@ -81,8 +90,7 @@ HashSet excludedMethods if (method.ReturnType != typeof(void)) { // Make sure the decorator method returned the same value as the decorated method: - decoratorMethodReturnValue - .Should().Be(decoratedMethodReturnValue); + decoratorMethodReturnValue.Should().Be(decoratedMethodReturnValue); } // Make sure the decorated method was called with the same arguments as the decorator method: @@ -97,25 +105,15 @@ HashSet excludedMethods throw new( $""" - The forward call assertion failed for the following method: - Type: {decoratorType.FullName} - Method: {method} + The forward call assertion failed for the following method: + Type: {decoratorType.FullName} + Method: {method} - Failure: - {ex} - """ + Failure: + {ex} + """ ); } } } - - /// - /// The method. - /// - private static readonly MethodInfo specimenFactoryCreateMethod = typeof(SpecimenFactory) - .GetMethod( - nameof(SpecimenFactory.Create), - BindingFlags.Public | BindingFlags.Static, - [typeof(ISpecimenBuilder)] - )!; } diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs index 5afe39a..a787161 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/DbConnectionPlusConfigurationTests.cs @@ -21,18 +21,16 @@ public void EnumSerializationMode_Integers_ShouldSerializeEnumAsInteger() DbParameter? interceptedDbParameter = null; - DbConnectionPlusConfiguration.Instance.InterceptDbCommand = - (command, _) => interceptedDbParameter = command.Parameters[0]; + DbConnectionPlusConfiguration.Instance.InterceptDbCommand = (command, _) => + interceptedDbParameter = command.Parameters[0]; DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; this.MockDbConnection.ExecuteNonQuery($"SELECT {Parameter(enumValue)}"); - interceptedDbParameter - .Should().NotBeNull(); + interceptedDbParameter.Should().NotBeNull(); - interceptedDbParameter.Value - .Should().Be((Int32)enumValue); + interceptedDbParameter.Value.Should().Be((int)enumValue); } [Fact] @@ -42,18 +40,16 @@ public void EnumSerializationMode_Strings_ShouldSerializeEnumAsString() DbParameter? interceptedDbParameter = null; - DbConnectionPlusConfiguration.Instance.InterceptDbCommand = - (command, _) => interceptedDbParameter = command.Parameters[0]; + DbConnectionPlusConfiguration.Instance.InterceptDbCommand = (command, _) => + interceptedDbParameter = command.Parameters[0]; DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; this.MockDbConnection.ExecuteNonQuery($"SELECT {Parameter(enumValue)}"); - interceptedDbParameter - .Should().NotBeNull(); + interceptedDbParameter.Should().NotBeNull(); - interceptedDbParameter.Value - .Should().Be(enumValue.ToString()); + interceptedDbParameter.Value.Should().Be(enumValue.ToString()); } [Fact] @@ -72,40 +68,47 @@ public void Freeze_ShouldFreezeConfigurationAndEntityTypeBuilders() ((IFreezable)configuration).Freeze(); Invoking(() => configuration.EnumSerializationMode = EnumSerializationMode.Integers) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => configuration.InterceptDbCommand = null) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => configuration.Entity()) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => entityTypeBuilder.ToTable("Entities")) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => entityTypeBuilder.Property(a => a.Id)) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => entityPropertyBuilder.IsKey()) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); } [Fact] public void GetDatabaseAdapter_NoAdapterRegisteredForConnectionType_ShouldThrow() => Invoking(() => DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionC))) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "No database adapter is registered for the database connection of the type " + - $"{typeof(FakeConnectionC)}. 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 " + + $"{typeof(FakeConnectionC)}. 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)}." ); [Fact] @@ -117,11 +120,9 @@ public void GetDatabaseAdapter_ShouldGetAdapter() DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterA); DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterB); - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)) - .Should().BeSameAs(adapterA); + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)).Should().BeSameAs(adapterA); - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionB)) - .Should().BeSameAs(adapterB); + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionB)).Should().BeSameAs(adapterB); } [Fact] @@ -133,20 +134,30 @@ public void GetDatabaseAdapter_ShouldGetRegisteredAdapters() DbConnectionPlusConfiguration.Instance.UseSqlite(); DbConnectionPlusConfiguration.Instance.UseSqlServer(); - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(MySqlConnection)) - .Should().BeOfType(); - - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(OracleConnection)) - .Should().BeOfType(); - - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(NpgsqlConnection)) - .Should().BeOfType(); - - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(SqliteConnection)) - .Should().BeOfType(); - - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(SqlConnection)) - .Should().BeOfType(); + DbConnectionPlusConfiguration + .Instance.GetDatabaseAdapter(typeof(MySqlConnection)) + .Should() + .BeOfType(); + + DbConnectionPlusConfiguration + .Instance.GetDatabaseAdapter(typeof(OracleConnection)) + .Should() + .BeOfType(); + + DbConnectionPlusConfiguration + .Instance.GetDatabaseAdapter(typeof(NpgsqlConnection)) + .Should() + .BeOfType(); + + DbConnectionPlusConfiguration + .Instance.GetDatabaseAdapter(typeof(SqliteConnection)) + .Should() + .BeOfType(); + + DbConnectionPlusConfiguration + .Instance.GetDatabaseAdapter(typeof(SqlConnection)) + .Should() + .BeOfType(); } [Fact] @@ -159,17 +170,11 @@ public void GetEntityTypeBuilders_ShouldGetConfiguredBuilders() var entityTypeBuilders = configuration.GetEntityTypeBuilders(); - entityTypeBuilders - .Should().ContainKeys( - typeof(Entity), - typeof(MappingTestEntityFluentApi) - ); + entityTypeBuilders.Should().ContainKeys(typeof(Entity), typeof(MappingTestEntityFluentApi)); - entityTypeBuilders[typeof(Entity)] - .Should().BeSameAs(entityBuilder); + entityTypeBuilders[typeof(Entity)].Should().BeSameAs(entityBuilder); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)] - .Should().BeSameAs(mappingTestEntityFluentApiBuilder); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].Should().BeSameAs(mappingTestEntityFluentApiBuilder); } [Fact] @@ -182,17 +187,13 @@ public void InterceptDbCommand_ShouldInterceptDbCommands() interceptor .WhenForAnyArgs(interceptor2 => - interceptor2.Invoke( - Arg.Any(), - Arg.Any>() - ) + interceptor2.Invoke(Arg.Any(), Arg.Any>()) ) .Do(info => - { - interceptedDbCommand = info.Arg(); - interceptedTemporaryTables = info.Arg>(); - } - ); + { + interceptedDbCommand = info.Arg(); + interceptedTemporaryTables = info.Arg>(); + }); DbConnectionPlusConfiguration.Instance.InterceptDbCommand = interceptor; @@ -200,12 +201,11 @@ public void InterceptDbCommand_ShouldInterceptDbCommands() var entityIds = Generate.Ids(); var stringValue = entities[0].StringValue; - InterpolatedSqlStatement statement = - $""" - SELECT Id, StringValue - FROM {TemporaryTable(entities)} TEntity - WHERE TEntity.Id IN ({TemporaryTable(entityIds)}) OR StringValue = {Parameter(stringValue)} - """; + InterpolatedSqlStatement statement = $""" + SELECT Id, StringValue + FROM {TemporaryTable(entities)} TEntity + WHERE TEntity.Id IN ({TemporaryTable(entityIds)}) OR StringValue = {Parameter(stringValue)} + """; var temporaryTables = statement.TemporaryTables; @@ -213,54 +213,45 @@ WHERE TEntity.Id IN ({TemporaryTable(entityIds)}) OR StringValue = {Parameter(s var timeout = Generate.Single(); var cancellationToken = Generate.Single(); - _ = this.MockDbConnection.Query( - statement, - transaction, - timeout, - CommandType.StoredProcedure, - cancellationToken - ).ToList(); - - interceptor.Received().Invoke( - Arg.Any(), - Arg.Any>() - ); + _ = this + .MockDbConnection.Query( + statement, + transaction, + timeout, + CommandType.StoredProcedure, + cancellationToken + ) + .ToList(); - interceptedDbCommand - .Should().NotBeNull(); + interceptor.Received().Invoke(Arg.Any(), Arg.Any>()); - interceptedDbCommand.CommandText - .Should().Be( + interceptedDbCommand.Should().NotBeNull(); + + interceptedDbCommand + .CommandText.Should() + .Be( $""" - SELECT Id, StringValue - FROM [#{temporaryTables[0].Name}] TEntity - WHERE TEntity.Id IN ([#{temporaryTables[1].Name}]) OR StringValue = @StringValue - """ + SELECT Id, StringValue + FROM [#{temporaryTables[0].Name}] TEntity + WHERE TEntity.Id IN ([#{temporaryTables[1].Name}]) OR StringValue = @StringValue + """ ); - interceptedDbCommand.Transaction - .Should().Be(transaction); + interceptedDbCommand.Transaction.Should().Be(transaction); - interceptedDbCommand.CommandType - .Should().Be(CommandType.StoredProcedure); + interceptedDbCommand.CommandType.Should().Be(CommandType.StoredProcedure); - interceptedDbCommand.CommandTimeout - .Should().Be((Int32)timeout.TotalSeconds); + interceptedDbCommand.CommandTimeout.Should().Be((int)timeout.TotalSeconds); - interceptedDbCommand.Parameters.Count - .Should().Be(1); + interceptedDbCommand.Parameters.Count.Should().Be(1); - interceptedDbCommand.Parameters[0].ParameterName - .Should().Be("StringValue"); + interceptedDbCommand.Parameters[0].ParameterName.Should().Be("StringValue"); - interceptedDbCommand.Parameters[0].Value - .Should().Be(stringValue); + interceptedDbCommand.Parameters[0].Value.Should().Be(stringValue); - interceptedTemporaryTables - .Should().NotBeNull(); + interceptedTemporaryTables.Should().NotBeNull(); - interceptedTemporaryTables - .Should().BeEquivalentTo(temporaryTables); + interceptedTemporaryTables.Should().BeEquivalentTo(temporaryTables); } [Fact] @@ -270,15 +261,13 @@ public void RegisterDatabaseAdapter_ShouldRegisterAdapter() DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterA); - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)) - .Should().BeSameAs(adapterA); + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)).Should().BeSameAs(adapterA); var adapterB = Substitute.For(); DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterB); - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionB)) - .Should().BeSameAs(adapterB); + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionB)).Should().BeSameAs(adapterB); } [Fact] @@ -288,15 +277,13 @@ public void RegisterDatabaseAdapter_ShouldReplaceRegisteredAdapter() DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterA); - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)) - .Should().BeSameAs(adapterA); + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)).Should().BeSameAs(adapterA); var adapterB = Substitute.For(); DbConnectionPlusConfiguration.Instance.RegisterDatabaseAdapter(adapterB); - DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)) - .Should().BeSameAs(adapterB); + DbConnectionPlusConfiguration.Instance.GetDatabaseAdapter(typeof(FakeConnectionA)).Should().BeSameAs(adapterB); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs index ad2d6dc..16af487 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityPropertyBuilderTests.cs @@ -9,8 +9,7 @@ public void ColumnName_Configured_ShouldReturnColumnName() builder.HasColumnName("Identifier"); - ((IEntityPropertyBuilder)builder).ColumnName - .Should().Be("Identifier"); + ((IEntityPropertyBuilder)builder).ColumnName.Should().Be("Identifier"); } [Fact] @@ -18,8 +17,7 @@ public void ColumnName_NotConfigured_ShouldReturnNull() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).ColumnName - .Should().BeNull(); + ((IEntityPropertyBuilder)builder).ColumnName.Should().BeNull(); } [Fact] @@ -30,31 +28,38 @@ public void Freeze_ShouldFreezeBuilder() ((IFreezable)builder).Freeze(); Invoking(() => builder.HasColumnName("Identifier")) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsComputed()) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsConcurrencyToken()) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsIdentity()) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsIgnored()) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsKey()) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.IsRowVersion()) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); } @@ -65,8 +70,7 @@ public void HasColumnName_ShouldSetColumnName() builder.HasColumnName("Identifier"); - ((IEntityPropertyBuilder)builder).ColumnName - .Should().Be("Identifier"); + ((IEntityPropertyBuilder)builder).ColumnName.Should().Be("Identifier"); } [Fact] @@ -76,8 +80,7 @@ public void IsComputed_Configured_ShouldReturnTrue() builder.IsComputed(); - ((IEntityPropertyBuilder)builder).IsComputed - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsComputed.Should().BeTrue(); } [Fact] @@ -85,8 +88,7 @@ public void IsComputed_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).IsComputed - .Should().BeFalse(); + ((IEntityPropertyBuilder)builder).IsComputed.Should().BeFalse(); } [Fact] @@ -96,8 +98,7 @@ public void IsComputed_ShouldMarkPropertyAsComputed() builder.IsComputed(); - ((IEntityPropertyBuilder)builder).IsComputed - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsComputed.Should().BeTrue(); } [Fact] @@ -107,8 +108,7 @@ public void IsConcurrencyToken_Configured_ShouldReturnTrue() builder.IsConcurrencyToken(); - ((IEntityPropertyBuilder)builder).IsConcurrencyToken - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsConcurrencyToken.Should().BeTrue(); } [Fact] @@ -116,8 +116,7 @@ public void IsConcurrencyToken_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).IsConcurrencyToken - .Should().BeFalse(); + ((IEntityPropertyBuilder)builder).IsConcurrencyToken.Should().BeFalse(); } [Fact] @@ -127,8 +126,7 @@ public void IsConcurrencyToken_ShouldMarkPropertyAsConcurrencyToken() builder.IsConcurrencyToken(); - ((IEntityPropertyBuilder)builder).IsConcurrencyToken - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsConcurrencyToken.Should().BeTrue(); } [Fact] @@ -138,8 +136,7 @@ public void IsIdentity_Configured_ShouldReturnTrue() builder.IsIdentity(); - ((IEntityPropertyBuilder)builder).IsIdentity - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsIdentity.Should().BeTrue(); } [Fact] @@ -147,8 +144,7 @@ public void IsIdentity_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).IsIdentity - .Should().BeFalse(); + ((IEntityPropertyBuilder)builder).IsIdentity.Should().BeFalse(); } [Fact] @@ -161,10 +157,11 @@ public void IsIdentity_OtherPropertyIsAlreadyMarked_ShouldThrow() var propertyBuilder = new EntityPropertyBuilder(entityTypeBuilder, "NotId"); Invoking(() => propertyBuilder.IsIdentity()) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "There is already the property 'Id' marked as an identity property for the entity type " + - $"{typeof(Entity)}. Only one property can be marked as identity property per entity type." + "There is already the property 'Id' marked as an identity property for the entity type " + + $"{typeof(Entity)}. Only one property can be marked as identity property per entity type." ); } @@ -175,8 +172,7 @@ public void IsIdentity_ShouldMarkPropertyAsIdentity() builder.IsIdentity(); - ((IEntityPropertyBuilder)builder).IsIdentity - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsIdentity.Should().BeTrue(); } [Fact] @@ -186,8 +182,7 @@ public void IsIgnored_Configured_ShouldReturnTrue() builder.IsIgnored(); - ((IEntityPropertyBuilder)builder).IsIgnored - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsIgnored.Should().BeTrue(); } [Fact] @@ -195,8 +190,7 @@ public void IsIgnored_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).IsIgnored - .Should().BeFalse(); + ((IEntityPropertyBuilder)builder).IsIgnored.Should().BeFalse(); } [Fact] @@ -206,8 +200,7 @@ public void IsIgnored_ShouldMarkPropertyAsIgnored() builder.IsIgnored(); - ((IEntityPropertyBuilder)builder).IsIgnored - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsIgnored.Should().BeTrue(); } [Fact] @@ -217,8 +210,7 @@ public void IsKey_Configured_ShouldReturnTrue() builder.IsKey(); - ((IEntityPropertyBuilder)builder).IsKey - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsKey.Should().BeTrue(); } [Fact] @@ -226,8 +218,7 @@ public void IsKey_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).IsKey - .Should().BeFalse(); + ((IEntityPropertyBuilder)builder).IsKey.Should().BeFalse(); } [Fact] @@ -237,8 +228,7 @@ public void IsKey_ShouldMarkPropertyAsKey() builder.IsKey(); - ((IEntityPropertyBuilder)builder).IsKey - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsKey.Should().BeTrue(); } [Fact] @@ -248,8 +238,7 @@ public void IsRowVersion_Configured_ShouldReturnTrue() builder.IsRowVersion(); - ((IEntityPropertyBuilder)builder).IsRowVersion - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsRowVersion.Should().BeTrue(); } [Fact] @@ -257,8 +246,7 @@ public void IsRowVersion_NotConfigured_ShouldReturnFalse() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).IsRowVersion - .Should().BeFalse(); + ((IEntityPropertyBuilder)builder).IsRowVersion.Should().BeFalse(); } [Fact] @@ -268,8 +256,7 @@ public void IsRowVersion_ShouldMarkPropertyAsRowVersion() builder.IsRowVersion(); - ((IEntityPropertyBuilder)builder).IsRowVersion - .Should().BeTrue(); + ((IEntityPropertyBuilder)builder).IsRowVersion.Should().BeTrue(); } [Fact] @@ -277,8 +264,7 @@ public void PropertyName_ShouldReturnPropertyName() { var builder = new EntityPropertyBuilder(Substitute.For(), "Property"); - ((IEntityPropertyBuilder)builder).PropertyName - .Should().Be("Property"); + ((IEntityPropertyBuilder)builder).PropertyName.Should().Be("Property"); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs index 77c08ff..bc7f492 100644 --- a/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Configuration/EntityTypeBuilderTests.cs @@ -14,70 +14,67 @@ public void Freeze_ShouldFreezeBuilderAndAllPropertyBuilders() ((IFreezable)builder).Freeze(); Invoking(() => builder.ToTable("Entities2")) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.Property(a => a.Id).HasColumnName("Identifier")) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); Invoking(() => builder.Property(a => a.StringValue).HasColumnName("String")) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); } [Fact] - public void Property_InvalidExpression_ShouldThrow() + public void PropertyBuilders_ShouldGetBuildersOfConfiguredProperties() { var builder = new EntityTypeBuilder(); - Invoking(() => builder.Property(a => a.Id.ToString())) - .Should().Throw() - .WithMessage( - "The expression 'a => a.Id.ToString()' is not a valid property access expression. The expression " + - "should represent a simple property access: 'a => a.MyProperty'.*" - ); - } + builder.Property(a => a.Id).IsKey(); + builder.Property(a => a.StringValue).IsComputed(); + builder.Property(a => a.Int64Value).IsIgnored(); - [Fact] - public void Property_ShouldGetPropertyBuilder() - { - var builder = new EntityTypeBuilder(); + var propertyBuilders = ((IEntityTypeBuilder)builder).PropertyBuilders; - var propertyBuilder = builder.Property(a => a.Id); + propertyBuilders.Should().HaveCount(3); + + propertyBuilders.Should().ContainKeys("Id", "StringValue", "Int64Value"); + + propertyBuilders["Id"].Should().BeSameAs(builder.Property(a => a.Id)); - propertyBuilder - .Should().NotBeNull(); + propertyBuilders["StringValue"].Should().BeSameAs(builder.Property(a => a.StringValue)); - builder.Property(a => a.Id) - .Should().BeSameAs(propertyBuilder); + propertyBuilders["Int64Value"].Should().BeSameAs(builder.Property(a => a.Int64Value)); } [Fact] - public void PropertyBuilders_ShouldGetBuildersOfConfiguredProperties() + public void Property_InvalidExpression_ShouldThrow() { var builder = new EntityTypeBuilder(); - builder.Property(a => a.Id).IsKey(); - builder.Property(a => a.StringValue).IsComputed(); - builder.Property(a => a.Int64Value).IsIgnored(); - - var propertyBuilders = ((IEntityTypeBuilder)builder).PropertyBuilders; - - propertyBuilders - .Should().HaveCount(3); + Invoking(() => builder.Property(a => a.Id.ToString())) + .Should() + .Throw() + .WithMessage( + "The expression 'a => a.Id.ToString()' is not a valid property access expression. The expression " + + "should represent a simple property access: 'a => a.MyProperty'.*" + ); + } - propertyBuilders - .Should().ContainKeys("Id", "StringValue", "Int64Value"); + [Fact] + public void Property_ShouldGetPropertyBuilder() + { + var builder = new EntityTypeBuilder(); - propertyBuilders["Id"] - .Should().BeSameAs(builder.Property(a => a.Id)); + var propertyBuilder = builder.Property(a => a.Id); - propertyBuilders["StringValue"] - .Should().BeSameAs(builder.Property(a => a.StringValue)); + propertyBuilder.Should().NotBeNull(); - propertyBuilders["Int64Value"] - .Should().BeSameAs(builder.Property(a => a.Int64Value)); + builder.Property(a => a.Id).Should().BeSameAs(propertyBuilder); } [Fact] @@ -85,9 +82,7 @@ public void ShouldGuardAgainstNullArguments() { var builder = new EntityTypeBuilder(); - ArgumentNullGuardVerifier.Verify(() => - builder.Property(a => a.Id) - ); + ArgumentNullGuardVerifier.Verify(() => builder.Property(a => a.Id)); } [Fact] @@ -95,8 +90,7 @@ public void TableName_NotConfigured_ShouldReturnNull() { var builder = new EntityTypeBuilder(); - ((IEntityTypeBuilder)builder).TableName - .Should().BeNull(); + ((IEntityTypeBuilder)builder).TableName.Should().BeNull(); } [Fact] @@ -106,8 +100,7 @@ public void ToTable_Configured_ShouldGetTableName() builder.ToTable("Entities"); - ((IEntityTypeBuilder)builder).TableName - .Should().Be("Entities"); + ((IEntityTypeBuilder)builder).TableName.Should().Be("Entities"); } [Fact] @@ -117,7 +110,6 @@ public void ToTable_ShouldSetTableName() builder.ToTable("Entities"); - ((IEntityTypeBuilder)builder).TableName - .Should().Be("Entities"); + ((IEntityTypeBuilder)builder).TableName.Should().Be("Entities"); } } diff --git a/tests/DbConnectionPlus.UnitTests/Converters/EnumConverterTests.cs b/tests/DbConnectionPlus.UnitTests/Converters/EnumConverterTests.cs index 8e96c4c..f7eec34 100644 --- a/tests/DbConnectionPlus.UnitTests/Converters/EnumConverterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Converters/EnumConverterTests.cs @@ -4,286 +4,288 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Converters; public class EnumConverterTests : UnitTestsBase { + public static IEnumerable<(object value, TestEnum expectedResult)> GetConvertValueToEnumMemberTestData() => + [ + ((short)1, TestEnum.Value1), + ((short)2, TestEnum.Value2), + ((short)3, TestEnum.Value3), + ((short)4, TestEnum.Value4), + ((short)5, TestEnum.Value5), + (1, TestEnum.Value1), + (2, TestEnum.Value2), + (3, TestEnum.Value3), + (4, TestEnum.Value4), + (5, TestEnum.Value5), + (1L, TestEnum.Value1), + (2L, TestEnum.Value2), + (3L, TestEnum.Value3), + (4L, TestEnum.Value4), + (5L, TestEnum.Value5), + ((byte)1, TestEnum.Value1), + ((byte)2, TestEnum.Value2), + ((byte)3, TestEnum.Value3), + ((byte)4, TestEnum.Value4), + ((byte)5, TestEnum.Value5), + ((float)1.0, TestEnum.Value1), + ((float)2.0, TestEnum.Value2), + ((float)3.0, TestEnum.Value3), + ((float)4.0, TestEnum.Value4), + ((float)5.0, TestEnum.Value5), + (1.0, TestEnum.Value1), + (2.0, TestEnum.Value2), + (3.0, TestEnum.Value3), + (4.0, TestEnum.Value4), + (5.0, TestEnum.Value5), + ((decimal)1.0, TestEnum.Value1), + ((decimal)2.0, TestEnum.Value2), + ((decimal)3.0, TestEnum.Value3), + ((decimal)4.0, TestEnum.Value4), + ((decimal)5.0, TestEnum.Value5), + ("Value1", TestEnum.Value1), + ("Value2", TestEnum.Value2), + ("Value3", TestEnum.Value3), + ("Value4", TestEnum.Value4), + ("Value5", TestEnum.Value5), + ("VALUE1", TestEnum.Value1), + ("VALUE2", TestEnum.Value2), + ("VALUE3", TestEnum.Value3), + ("VALUE4", TestEnum.Value4), + ("VALUE5", TestEnum.Value5), + ("1", TestEnum.Value1), + ("2", TestEnum.Value2), + ("3", TestEnum.Value3), + ("4", TestEnum.Value4), + ("5", TestEnum.Value5), + (TestEnum.Value1, TestEnum.Value1), + (TestEnum.Value2, TestEnum.Value2), + (TestEnum.Value3, TestEnum.Value3), + (TestEnum.Value4, TestEnum.Value4), + (TestEnum.Value5, TestEnum.Value5), + ]; + [Fact] - public void ConvertValueToEnumMember_EmptyStringValue_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(String.Empty, typeof(TestEnum))) - .Should().Throw() + public void ConvertValueToEnumMemberOfT_EmptyStringValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(string.Empty)) + .Should() + .Throw() .WithMessage( - "Could not convert an empty string or a string that consists only of white-space characters to an " + - $"enum member of the type {typeof(TestEnum)}." + "Could not convert an empty string or a string that consists only of white-space characters to an " + + $"enum member of the type {typeof(TestEnum)}." ); [Fact] - public void ConvertValueToEnumMember_NonEnumTargetType_ShouldThrow() + public void ConvertValueToEnumMemberOfT_NonEnumTargetType_ShouldThrow() { - Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA", typeof(Int32))) - .Should().Throw() + Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA")) + .Should() + .Throw() .WithMessage( - $"Could not convert the value 'ValueA' ({typeof(String)}) to an enum member of the type " + - $"{typeof(Int32)}, because the type {typeof(Int32)} is not an enum type.*" + $"Could not convert the value 'ValueA' ({typeof(string)}) to an enum member of the type " + + $"{typeof(int)}, because the type {typeof(int)} is not an enum type.*" ); - Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA", typeof(Int32?))) - .Should().Throw() + Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA")) + .Should() + .Throw() .WithMessage( - $"Could not convert the value 'ValueA' ({typeof(String)}) to an enum member of the type " + - $"{typeof(Int32?)}, because the type {typeof(Int32?)} is not an enum type.*" + $"Could not convert the value 'ValueA' ({typeof(string)}) to an enum member of the type " + + $"{typeof(int?)}, because the type {typeof(int?)} is not an enum type.*" ); } [Fact] - public void ConvertValueToEnumMember_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() + public void ConvertValueToEnumMemberOfT_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() { - Invoking(() => EnumConverter.ConvertValueToEnumMember(DBNull.Value, typeof(TestEnum))) - .Should().Throw() - .WithMessage( - $"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}." - ); + Invoking(() => EnumConverter.ConvertValueToEnumMember(DBNull.Value)) + .Should() + .Throw() + .WithMessage($"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}."); - Invoking(() => EnumConverter.ConvertValueToEnumMember(null, typeof(TestEnum))) - .Should().Throw() - .WithMessage( - $"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}." - ); + Invoking(() => EnumConverter.ConvertValueToEnumMember(null)) + .Should() + .Throw() + .WithMessage($"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}."); } [Fact] - public void ConvertValueToEnumMember_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() + public void ConvertValueToEnumMemberOfT_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() { - EnumConverter.ConvertValueToEnumMember(DBNull.Value, typeof(TestEnum?)) - .Should().BeNull(); + EnumConverter.ConvertValueToEnumMember(DBNull.Value).Should().BeNull(); - EnumConverter.ConvertValueToEnumMember(null, typeof(TestEnum?)) - .Should().BeNull(); + EnumConverter.ConvertValueToEnumMember(null).Should().BeNull(); } [Fact] - public void ConvertValueToEnumMember_NumericValueNotMatchingAnyEnumMemberValue_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(999, typeof(TestEnum))) - .Should().Throw() + public void ConvertValueToEnumMemberOfT_NumericValueNotMatchingAnyEnumMemberValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(999)) + .Should() + .Throw() .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members." + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members." ); [Theory] [MemberData(nameof(GetConvertValueToEnumMemberTestData))] - public void - ConvertValueToEnumMember_ShouldConvertValueToEnumMember(Object value, TestEnum expectedResult) + public void ConvertValueToEnumMemberOfT_ShouldConvertValueToEnumMember(object value, TestEnum expectedResult) { - EnumConverter.ConvertValueToEnumMember(value, typeof(TestEnum)) - .Should().Be(expectedResult); + EnumConverter.ConvertValueToEnumMember(value).Should().Be(expectedResult); - EnumConverter.ConvertValueToEnumMember(value, typeof(TestEnum?)) - .Should().Be(expectedResult); + EnumConverter.ConvertValueToEnumMember(value).Should().Be(expectedResult); } [Fact] - public void ConvertValueToEnumMember_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember("NonExistent", typeof(TestEnum))) - .Should().Throw() + public void ConvertValueToEnumMemberOfT_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember("NonExistent")) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members." + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members." ); [Fact] - public void ConvertValueToEnumMember_ValueIsNeitherEnumValueNorStringNorNumeric_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(Guid.Empty, typeof(TestEnum))) - .Should().Throw() + public void ConvertValueToEnumMemberOfT_ValueIsNeitherEnumValueNorStringNorNumeric_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(Guid.Empty)) + .Should() + .Throw() .WithMessage( - $"Could not convert the value '{Guid.Empty}' ({typeof(Guid)}) to an enum member of the type " + - $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + - "value." + $"Could not convert the value '{Guid.Empty}' ({typeof(Guid)}) to an enum member of the type " + + $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + + "value." ); [Fact] - public void ConvertValueToEnumMember_ValueIsOfDifferentEnumType_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(ConsoleColor.Red, typeof(TestEnum))) - .Should().Throw() + public void ConvertValueToEnumMemberOfT_ValueIsOfDifferentEnumType_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(ConsoleColor.Red)) + .Should() + .Throw() .WithMessage( - $"Could not convert the value 'Red' ({typeof(ConsoleColor)}) to an enum member of the type " + - $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + - "value." + $"Could not convert the value 'Red' ({typeof(ConsoleColor)}) to an enum member of the type " + + $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + + "value." ); [Fact] - public void ConvertValueToEnumMember_WhitespaceStringValue_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(" ", typeof(TestEnum))) - .Should().Throw() + public void ConvertValueToEnumMemberOfT_WhitespaceStringValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(" ")) + .Should() + .Throw() .WithMessage( - "Could not convert an empty string or a string that consists only of white-space characters to an " + - $"enum member of the type {typeof(TestEnum)}." + "Could not convert an empty string or a string that consists only of white-space characters to an " + + $"enum member of the type {typeof(TestEnum)}." ); [Fact] - public void ConvertValueToEnumMemberOfT_EmptyStringValue_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(String.Empty)) - .Should().Throw() + public void ConvertValueToEnumMember_EmptyStringValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(string.Empty, typeof(TestEnum))) + .Should() + .Throw() .WithMessage( - "Could not convert an empty string or a string that consists only of white-space characters to an " + - $"enum member of the type {typeof(TestEnum)}." + "Could not convert an empty string or a string that consists only of white-space characters to an " + + $"enum member of the type {typeof(TestEnum)}." ); [Fact] - public void ConvertValueToEnumMemberOfT_NonEnumTargetType_ShouldThrow() + public void ConvertValueToEnumMember_NonEnumTargetType_ShouldThrow() { - Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA")) - .Should().Throw() + Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA", typeof(int))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value 'ValueA' ({typeof(String)}) to an enum member of the type " + - $"{typeof(Int32)}, because the type {typeof(Int32)} is not an enum type.*" + $"Could not convert the value 'ValueA' ({typeof(string)}) to an enum member of the type " + + $"{typeof(int)}, because the type {typeof(int)} is not an enum type.*" ); - Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA")) - .Should().Throw() + Invoking(() => EnumConverter.ConvertValueToEnumMember("ValueA", typeof(int?))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value 'ValueA' ({typeof(String)}) to an enum member of the type " + - $"{typeof(Int32?)}, because the type {typeof(Int32?)} is not an enum type.*" + $"Could not convert the value 'ValueA' ({typeof(string)}) to an enum member of the type " + + $"{typeof(int?)}, because the type {typeof(int?)} is not an enum type.*" ); } [Fact] - public void ConvertValueToEnumMemberOfT_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() + public void ConvertValueToEnumMember_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() { - Invoking(() => EnumConverter.ConvertValueToEnumMember(DBNull.Value)) - .Should().Throw() - .WithMessage( - $"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}." - ); + Invoking(() => EnumConverter.ConvertValueToEnumMember(DBNull.Value, typeof(TestEnum))) + .Should() + .Throw() + .WithMessage($"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}."); - Invoking(() => EnumConverter.ConvertValueToEnumMember(null)) - .Should().Throw() - .WithMessage( - $"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}." - ); + Invoking(() => EnumConverter.ConvertValueToEnumMember(null, typeof(TestEnum))) + .Should() + .Throw() + .WithMessage($"Could not convert {{null}} to an enum member of the type {typeof(TestEnum)}."); } [Fact] - public void ConvertValueToEnumMemberOfT_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() + public void ConvertValueToEnumMember_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() { - EnumConverter.ConvertValueToEnumMember(DBNull.Value) - .Should().BeNull(); + EnumConverter.ConvertValueToEnumMember(DBNull.Value, typeof(TestEnum?)).Should().BeNull(); - EnumConverter.ConvertValueToEnumMember(null) - .Should().BeNull(); + EnumConverter.ConvertValueToEnumMember(null, typeof(TestEnum?)).Should().BeNull(); } [Fact] - public void ConvertValueToEnumMemberOfT_NumericValueNotMatchingAnyEnumMemberValue_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(999)) - .Should().Throw() + public void ConvertValueToEnumMember_NumericValueNotMatchingAnyEnumMemberValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(999, typeof(TestEnum))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members." + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members." ); [Theory] [MemberData(nameof(GetConvertValueToEnumMemberTestData))] - public void - ConvertValueToEnumMemberOfT_ShouldConvertValueToEnumMember(Object value, TestEnum expectedResult) + public void ConvertValueToEnumMember_ShouldConvertValueToEnumMember(object value, TestEnum expectedResult) { - EnumConverter.ConvertValueToEnumMember(value) - .Should().Be(expectedResult); + EnumConverter.ConvertValueToEnumMember(value, typeof(TestEnum)).Should().Be(expectedResult); - EnumConverter.ConvertValueToEnumMember(value) - .Should().Be(expectedResult); + EnumConverter.ConvertValueToEnumMember(value, typeof(TestEnum?)).Should().Be(expectedResult); } [Fact] - public void ConvertValueToEnumMemberOfT_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember("NonExistent")) - .Should().Throw() + public void ConvertValueToEnumMember_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember("NonExistent", typeof(TestEnum))) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members." + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members." ); [Fact] - public void ConvertValueToEnumMemberOfT_ValueIsNeitherEnumValueNorStringNorNumeric_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(Guid.Empty)) - .Should().Throw() + public void ConvertValueToEnumMember_ValueIsNeitherEnumValueNorStringNorNumeric_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(Guid.Empty, typeof(TestEnum))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value '{Guid.Empty}' ({typeof(Guid)}) to an enum member of the type " + - $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + - "value." + $"Could not convert the value '{Guid.Empty}' ({typeof(Guid)}) to an enum member of the type " + + $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + + "value." ); [Fact] - public void ConvertValueToEnumMemberOfT_ValueIsOfDifferentEnumType_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(ConsoleColor.Red)) - .Should().Throw() + public void ConvertValueToEnumMember_ValueIsOfDifferentEnumType_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(ConsoleColor.Red, typeof(TestEnum))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value 'Red' ({typeof(ConsoleColor)}) to an enum member of the type " + - $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + - "value." + $"Could not convert the value 'Red' ({typeof(ConsoleColor)}) to an enum member of the type " + + $"{typeof(TestEnum)}. The value must either be an enum value of that type or a string or a numeric " + + "value." ); [Fact] - public void ConvertValueToEnumMemberOfT_WhitespaceStringValue_ShouldThrow() => - Invoking(() => EnumConverter.ConvertValueToEnumMember(" ")) - .Should().Throw() + public void ConvertValueToEnumMember_WhitespaceStringValue_ShouldThrow() => + Invoking(() => EnumConverter.ConvertValueToEnumMember(" ", typeof(TestEnum))) + .Should() + .Throw() .WithMessage( - "Could not convert an empty string or a string that consists only of white-space characters to an " + - $"enum member of the type {typeof(TestEnum)}." + "Could not convert an empty string or a string that consists only of white-space characters to an " + + $"enum member of the type {typeof(TestEnum)}." ); - - public static IEnumerable<(Object value, TestEnum expectedResult)> GetConvertValueToEnumMemberTestData() => - [ - ((Int16)1, TestEnum.Value1), - ((Int16)2, TestEnum.Value2), - ((Int16)3, TestEnum.Value3), - ((Int16)4, TestEnum.Value4), - ((Int16)5, TestEnum.Value5), - (1, TestEnum.Value1), - (2, TestEnum.Value2), - (3, TestEnum.Value3), - (4, TestEnum.Value4), - (5, TestEnum.Value5), - (1L, TestEnum.Value1), - (2L, TestEnum.Value2), - (3L, TestEnum.Value3), - (4L, TestEnum.Value4), - (5L, TestEnum.Value5), - ((Byte)1, TestEnum.Value1), - ((Byte)2, TestEnum.Value2), - ((Byte)3, TestEnum.Value3), - ((Byte)4, TestEnum.Value4), - ((Byte)5, TestEnum.Value5), - ((Single)1.0, TestEnum.Value1), - ((Single)2.0, TestEnum.Value2), - ((Single)3.0, TestEnum.Value3), - ((Single)4.0, TestEnum.Value4), - ((Single)5.0, TestEnum.Value5), - (1.0, TestEnum.Value1), - (2.0, TestEnum.Value2), - (3.0, TestEnum.Value3), - (4.0, TestEnum.Value4), - (5.0, TestEnum.Value5), - ((Decimal)1.0, TestEnum.Value1), - ((Decimal)2.0, TestEnum.Value2), - ((Decimal)3.0, TestEnum.Value3), - ((Decimal)4.0, TestEnum.Value4), - ((Decimal)5.0, TestEnum.Value5), - ("Value1", TestEnum.Value1), - ("Value2", TestEnum.Value2), - ("Value3", TestEnum.Value3), - ("Value4", TestEnum.Value4), - ("Value5", TestEnum.Value5), - ("VALUE1", TestEnum.Value1), - ("VALUE2", TestEnum.Value2), - ("VALUE3", TestEnum.Value3), - ("VALUE4", TestEnum.Value4), - ("VALUE5", TestEnum.Value5), - ("1", TestEnum.Value1), - ("2", TestEnum.Value2), - ("3", TestEnum.Value3), - ("4", TestEnum.Value4), - ("5", TestEnum.Value5), - (TestEnum.Value1, TestEnum.Value1), - (TestEnum.Value2, TestEnum.Value2), - (TestEnum.Value3, TestEnum.Value3), - (TestEnum.Value4, TestEnum.Value4), - (TestEnum.Value5, TestEnum.Value5) - ]; } diff --git a/tests/DbConnectionPlus.UnitTests/Converters/EnumSerializerTests.cs b/tests/DbConnectionPlus.UnitTests/Converters/EnumSerializerTests.cs index 648e58d..d813393 100644 --- a/tests/DbConnectionPlus.UnitTests/Converters/EnumSerializerTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Converters/EnumSerializerTests.cs @@ -7,7 +7,8 @@ public class EnumSerializerTests : UnitTestsBase [Fact] public void SerializeEnum_InvalidEnumSerializationMode_ShouldThrow() => Invoking(() => EnumSerializer.SerializeEnum(TestEnum.Value3, (EnumSerializationMode)999)) - .Should().Throw() + .Should() + .Throw() .WithMessage( $"The {nameof(EnumSerializationMode)} '999' ({typeof(EnumSerializationMode)}) is not supported.*" ); @@ -26,10 +27,8 @@ public void SerializeEnum_InvalidEnumSerializationMode_ShouldThrow() => public void SerializeEnum_ShouldSerializeEnumValueAccordingToSerializationMode( TestEnum enumValue, EnumSerializationMode enumSerializationMode, - Object expectedResult - ) => - EnumSerializer.SerializeEnum(enumValue, enumSerializationMode) - .Should().Be(expectedResult); + object expectedResult + ) => EnumSerializer.SerializeEnum(enumValue, enumSerializationMode).Should().Be(expectedResult); [Fact] public void ShouldGuardAgainstNullArguments() => diff --git a/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs b/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs index 9ba8097..80a95db 100644 --- a/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Converters/ValueConverterTests.cs @@ -14,14 +14,565 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Converters; public class ValueConverterTests : UnitTestsBase { + public static IEnumerable<( + Type SourceType, + Type TargetType, + bool ExpectedCanConvert, + object SourceValue, + object ExpectedTargetValue + )> GetConvertTestData() + { + var faker = new Faker(); + + // All numeric values are kept within the range 0-127, so they are convertible to the smallest target type + // (SByte) without overflow. + var byteValue = faker.Random.Byte(0, 127); + var charValue = faker.Random.Char('A', 'Z'); + var dateOnlyValue = faker.Date.PastDateOnly(); + var dateTimeValue = faker.Date.Past(); + var dateTimeOffsetValue = faker.Date.PastOffset(); + var decimalValue = faker.Random.Decimal(0, 127); + var doubleValue = faker.Random.Double(0, 127); + var guidValue = faker.Random.Guid(); + var int16Value = faker.Random.Short(0, 127); + var int32Value = faker.Random.Int(0, 127); + var int64Value = faker.Random.Long(0, 127); + var intPtrValue = (nint)faker.Random.Int(0, 127); + var sbyteValue = faker.Random.SByte(0); + var singleValue = faker.Random.Float(0, 127); + var stringValue = faker.Lorem.Sentence(); + var uint16Value = faker.Random.UShort(0, 127); + var uint32Value = faker.Random.UInt(0, 127); + var uint64Value = faker.Random.ULong(0, 127); + var timeSpanValue = faker.Date.Timespan(TimeSpan.FromHours(23)); + var timeOnlyValue = faker.Date.RecentTimeOnly(); + var uintPtrValue = (nuint)faker.Random.Int(0, 127); + var enumValue = faker.Random.Enum(); + + // @formatter:off + + return + [ + (typeof(bool), typeof(bool), true, true, true), + (typeof(bool), typeof(byte), true, true, (byte)1), + (typeof(bool), typeof(decimal), true, true, (decimal)1), + (typeof(bool), typeof(double), true, true, (double)1), + (typeof(bool), typeof(short), true, true, (short)1), + (typeof(bool), typeof(int), true, true, 1), + (typeof(bool), typeof(long), true, true, (long)1), + (typeof(bool), typeof(object), true, true, true), + (typeof(bool), typeof(sbyte), true, true, (sbyte)1), + (typeof(bool), typeof(float), true, true, (float)1), + (typeof(bool), typeof(string), true, true, "True"), + (typeof(bool), typeof(ushort), true, true, (ushort)1), + (typeof(bool), typeof(uint), true, true, (uint)1), + (typeof(bool), typeof(ulong), true, true, (ulong)1), + (typeof(byte), typeof(bool), true, (byte)1, true), + (typeof(byte), typeof(byte), true, byteValue, byteValue), + (typeof(byte), typeof(char), true, byteValue, (char)byteValue), + (typeof(byte), typeof(decimal), true, byteValue, (decimal)byteValue), + (typeof(byte), typeof(double), true, byteValue, (double)byteValue), + (typeof(byte), typeof(short), true, byteValue, (short)byteValue), + (typeof(byte), typeof(int), true, byteValue, (int)byteValue), + (typeof(byte), typeof(long), true, byteValue, (long)byteValue), + (typeof(byte), typeof(object), true, byteValue, byteValue), + (typeof(byte), typeof(sbyte), true, byteValue, (sbyte)byteValue), + (typeof(byte), typeof(float), true, byteValue, (float)byteValue), + (typeof(byte), typeof(string), true, byteValue, byteValue.ToString(CultureInfo.InvariantCulture)), + (typeof(byte), typeof(TestEnum), true, (byte)enumValue, enumValue), + (typeof(byte), typeof(ushort), true, byteValue, (ushort)byteValue), + (typeof(byte), typeof(uint), true, byteValue, (uint)byteValue), + (typeof(byte), typeof(ulong), true, byteValue, (ulong)byteValue), + (typeof(byte[]), typeof(Guid), true, guidValue.ToByteArray(), guidValue), + (typeof(char), typeof(byte), true, charValue, (byte)charValue), + (typeof(char), typeof(char), true, charValue, charValue), + (typeof(char), typeof(short), true, charValue, (short)charValue), + (typeof(char), typeof(int), true, charValue, (int)charValue), + (typeof(char), typeof(long), true, charValue, (long)charValue), + (typeof(char), typeof(object), true, charValue, charValue), + (typeof(char), typeof(sbyte), true, charValue, (sbyte)charValue), + (typeof(char), typeof(string), true, charValue, charValue.ToString(CultureInfo.InvariantCulture)), + (typeof(char), typeof(ushort), true, charValue, (ushort)charValue), + (typeof(char), typeof(uint), true, charValue, (uint)charValue), + (typeof(char), typeof(ulong), true, charValue, (ulong)charValue), + (typeof(DateOnly), typeof(DateOnly), true, dateOnlyValue, dateOnlyValue), + (typeof(DateOnly), typeof(object), true, dateOnlyValue, dateOnlyValue), + ( + typeof(DateOnly), + typeof(string), + true, + dateOnlyValue, + dateOnlyValue.ToString("O", CultureInfo.InvariantCulture) + ), + (typeof(DateTime), typeof(DateOnly), true, dateOnlyValue.ToDateTime(TimeOnly.MinValue), dateOnlyValue), + (typeof(DateTime), typeof(DateTime), true, dateTimeValue, dateTimeValue), + (typeof(DateTime), typeof(object), true, dateTimeValue, dateTimeValue), + ( + typeof(DateTime), + typeof(string), + true, + dateTimeValue, + dateTimeValue.ToString("O", CultureInfo.InvariantCulture) + ), + (typeof(DateTimeOffset), typeof(DateTimeOffset), true, dateTimeOffsetValue, dateTimeOffsetValue), + (typeof(DateTimeOffset), typeof(object), true, dateTimeOffsetValue, dateTimeOffsetValue), + ( + typeof(DateTimeOffset), + typeof(string), + true, + dateTimeOffsetValue, + dateTimeOffsetValue.ToString("O", CultureInfo.InvariantCulture) + ), + (typeof(decimal), typeof(bool), true, 1M, true), + ( + typeof(decimal), + typeof(byte), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(byte), CultureInfo.InvariantCulture) + ), + (typeof(decimal), typeof(decimal), true, decimalValue, decimalValue), + ( + typeof(decimal), + typeof(double), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(double), CultureInfo.InvariantCulture) + ), + ( + typeof(decimal), + typeof(short), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(short), CultureInfo.InvariantCulture) + ), + ( + typeof(decimal), + typeof(int), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(int), CultureInfo.InvariantCulture) + ), + ( + typeof(decimal), + typeof(long), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(long), CultureInfo.InvariantCulture) + ), + (typeof(decimal), typeof(object), true, decimalValue, decimalValue), + ( + typeof(decimal), + typeof(sbyte), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(sbyte), CultureInfo.InvariantCulture) + ), + ( + typeof(decimal), + typeof(float), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(float), CultureInfo.InvariantCulture) + ), + (typeof(decimal), typeof(string), true, decimalValue, decimalValue.ToString(CultureInfo.InvariantCulture)), + (typeof(decimal), typeof(TestEnum), true, (decimal)enumValue, enumValue), + ( + typeof(decimal), + typeof(ushort), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(ushort), CultureInfo.InvariantCulture) + ), + ( + typeof(decimal), + typeof(uint), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(uint), CultureInfo.InvariantCulture) + ), + ( + typeof(decimal), + typeof(ulong), + true, + decimalValue, + Convert.ChangeType(decimalValue, typeof(ulong), CultureInfo.InvariantCulture) + ), + (typeof(double), typeof(bool), true, 1.0, true), + ( + typeof(double), + typeof(byte), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(byte), CultureInfo.InvariantCulture) + ), + ( + typeof(double), + typeof(decimal), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(decimal), CultureInfo.InvariantCulture) + ), + (typeof(double), typeof(double), true, doubleValue, doubleValue), + ( + typeof(double), + typeof(short), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(short), CultureInfo.InvariantCulture) + ), + ( + typeof(double), + typeof(int), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(int), CultureInfo.InvariantCulture) + ), + ( + typeof(double), + typeof(long), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(long), CultureInfo.InvariantCulture) + ), + (typeof(double), typeof(object), true, doubleValue, doubleValue), + ( + typeof(double), + typeof(sbyte), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(sbyte), CultureInfo.InvariantCulture) + ), + ( + typeof(double), + typeof(float), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(float), CultureInfo.InvariantCulture) + ), + (typeof(double), typeof(string), true, doubleValue, doubleValue.ToString(CultureInfo.InvariantCulture)), + (typeof(double), typeof(TestEnum), true, (double)enumValue, enumValue), + ( + typeof(double), + typeof(ushort), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(ushort), CultureInfo.InvariantCulture) + ), + ( + typeof(double), + typeof(uint), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(uint), CultureInfo.InvariantCulture) + ), + ( + typeof(double), + typeof(ulong), + true, + doubleValue, + Convert.ChangeType(doubleValue, typeof(ulong), CultureInfo.InvariantCulture) + ), + (typeof(Guid), typeof(byte[]), true, guidValue, guidValue.ToByteArray()), + (typeof(Guid), typeof(Guid), true, guidValue, guidValue), + (typeof(Guid), typeof(object), true, guidValue, guidValue), + (typeof(Guid), typeof(string), true, guidValue, guidValue.ToString("D")), + (typeof(short), typeof(bool), true, (short)1, true), + (typeof(short), typeof(byte), true, int16Value, (byte)int16Value), + (typeof(short), typeof(char), true, int16Value, (char)int16Value), + (typeof(short), typeof(decimal), true, int16Value, (decimal)int16Value), + (typeof(short), typeof(double), true, int16Value, (double)int16Value), + (typeof(short), typeof(short), true, int16Value, int16Value), + (typeof(short), typeof(int), true, int16Value, (int)int16Value), + (typeof(short), typeof(long), true, int16Value, (long)int16Value), + (typeof(short), typeof(object), true, int16Value, int16Value), + (typeof(short), typeof(sbyte), true, int16Value, (sbyte)int16Value), + (typeof(short), typeof(float), true, int16Value, (float)int16Value), + (typeof(short), typeof(string), true, int16Value, int16Value.ToString(CultureInfo.InvariantCulture)), + (typeof(short), typeof(TestEnum), true, (short)enumValue, enumValue), + (typeof(short), typeof(ushort), true, int16Value, (ushort)int16Value), + (typeof(short), typeof(uint), true, int16Value, (uint)int16Value), + (typeof(short), typeof(ulong), true, int16Value, (ulong)int16Value), + (typeof(int), typeof(bool), true, 1, true), + (typeof(int), typeof(byte), true, int32Value, (byte)int32Value), + (typeof(int), typeof(char), true, int32Value, (char)int32Value), + (typeof(int), typeof(decimal), true, int32Value, (decimal)int32Value), + (typeof(int), typeof(double), true, int32Value, (double)int32Value), + (typeof(int), typeof(short), true, int32Value, (short)int32Value), + (typeof(int), typeof(int), true, int32Value, int32Value), + (typeof(int), typeof(long), true, int32Value, (long)int32Value), + (typeof(int), typeof(object), true, int32Value, int32Value), + (typeof(int), typeof(sbyte), true, int32Value, (sbyte)int32Value), + (typeof(int), typeof(float), true, int32Value, (float)int32Value), + (typeof(int), typeof(string), true, int32Value, int32Value.ToString(CultureInfo.InvariantCulture)), + (typeof(int), typeof(TestEnum), true, (int)enumValue, enumValue), + (typeof(int), typeof(ushort), true, int32Value, (ushort)int32Value), + (typeof(int), typeof(uint), true, int32Value, (uint)int32Value), + (typeof(int), typeof(ulong), true, int32Value, (ulong)int32Value), + (typeof(long), typeof(bool), true, (long)1, true), + (typeof(long), typeof(byte), true, int64Value, (byte)int64Value), + (typeof(long), typeof(char), true, int64Value, (char)int64Value), + (typeof(long), typeof(decimal), true, int64Value, (decimal)int64Value), + (typeof(long), typeof(double), true, int64Value, (double)int64Value), + (typeof(long), typeof(short), true, int64Value, (short)int64Value), + (typeof(long), typeof(int), true, int64Value, (int)int64Value), + (typeof(long), typeof(long), true, int64Value, int64Value), + (typeof(long), typeof(object), true, int64Value, int64Value), + (typeof(long), typeof(sbyte), true, int64Value, (sbyte)int64Value), + (typeof(long), typeof(float), true, int64Value, (float)int64Value), + (typeof(long), typeof(string), true, int64Value, int64Value.ToString(CultureInfo.InvariantCulture)), + (typeof(long), typeof(TestEnum), true, (long)enumValue, enumValue), + (typeof(long), typeof(ushort), true, int64Value, (ushort)int64Value), + (typeof(long), typeof(uint), true, int64Value, (uint)int64Value), + (typeof(long), typeof(ulong), true, int64Value, (ulong)int64Value), + (typeof(nint), typeof(nint), true, intPtrValue, intPtrValue), + (typeof(nint), typeof(object), true, intPtrValue, intPtrValue), + (typeof(sbyte), typeof(bool), true, (sbyte)1, true), + (typeof(sbyte), typeof(byte), true, sbyteValue, (byte)sbyteValue), + (typeof(sbyte), typeof(char), true, sbyteValue, (char)sbyteValue), + (typeof(sbyte), typeof(decimal), true, sbyteValue, (decimal)sbyteValue), + (typeof(sbyte), typeof(double), true, sbyteValue, (double)sbyteValue), + (typeof(sbyte), typeof(short), true, sbyteValue, (short)sbyteValue), + (typeof(sbyte), typeof(int), true, sbyteValue, (int)sbyteValue), + (typeof(sbyte), typeof(long), true, sbyteValue, (long)sbyteValue), + (typeof(sbyte), typeof(object), true, sbyteValue, sbyteValue), + (typeof(sbyte), typeof(sbyte), true, sbyteValue, sbyteValue), + (typeof(sbyte), typeof(float), true, sbyteValue, (float)sbyteValue), + (typeof(sbyte), typeof(string), true, sbyteValue, sbyteValue.ToString(CultureInfo.InvariantCulture)), + (typeof(sbyte), typeof(TestEnum), true, (sbyte)enumValue, enumValue), + (typeof(sbyte), typeof(ushort), true, sbyteValue, (ushort)sbyteValue), + (typeof(sbyte), typeof(uint), true, sbyteValue, (uint)sbyteValue), + (typeof(sbyte), typeof(ulong), true, sbyteValue, (ulong)sbyteValue), + (typeof(float), typeof(bool), true, (float)1, true), + ( + typeof(float), + typeof(byte), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(byte), CultureInfo.InvariantCulture) + ), + ( + typeof(float), + typeof(decimal), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(decimal), CultureInfo.InvariantCulture) + ), + ( + typeof(float), + typeof(double), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(double), CultureInfo.InvariantCulture) + ), + ( + typeof(float), + typeof(short), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(short), CultureInfo.InvariantCulture) + ), + ( + typeof(float), + typeof(int), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(int), CultureInfo.InvariantCulture) + ), + ( + typeof(float), + typeof(long), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(long), CultureInfo.InvariantCulture) + ), + (typeof(float), typeof(object), true, singleValue, singleValue), + ( + typeof(float), + typeof(sbyte), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(sbyte), CultureInfo.InvariantCulture) + ), + ( + typeof(float), + typeof(float), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(float), CultureInfo.InvariantCulture) + ), + (typeof(float), typeof(string), true, singleValue, singleValue.ToString(CultureInfo.InvariantCulture)), + (typeof(float), typeof(TestEnum), true, (float)enumValue, enumValue), + ( + typeof(float), + typeof(ushort), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(ushort), CultureInfo.InvariantCulture) + ), + ( + typeof(float), + typeof(uint), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(uint), CultureInfo.InvariantCulture) + ), + ( + typeof(float), + typeof(ulong), + true, + singleValue, + Convert.ChangeType(singleValue, typeof(ulong), CultureInfo.InvariantCulture) + ), + (typeof(string), typeof(bool), true, "True", true), + (typeof(string), typeof(byte), true, byteValue.ToString(CultureInfo.InvariantCulture), byteValue), + (typeof(string), typeof(char), true, charValue.ToString(CultureInfo.InvariantCulture), charValue), + ( + typeof(string), + typeof(DateOnly), + true, + dateOnlyValue.ToString("O", CultureInfo.InvariantCulture), + dateOnlyValue + ), + ( + typeof(string), + typeof(DateTime), + true, + dateTimeValue.ToString("O", CultureInfo.InvariantCulture), + dateTimeValue + ), + ( + typeof(string), + typeof(DateTimeOffset), + true, + dateTimeOffsetValue.ToString("O", CultureInfo.InvariantCulture), + dateTimeOffsetValue + ), + (typeof(string), typeof(decimal), true, decimalValue.ToString(CultureInfo.InvariantCulture), decimalValue), + (typeof(string), typeof(double), true, doubleValue.ToString(CultureInfo.InvariantCulture), doubleValue), + (typeof(string), typeof(Guid), true, guidValue.ToString("D"), guidValue), + (typeof(string), typeof(short), true, int16Value.ToString(CultureInfo.InvariantCulture), int16Value), + (typeof(string), typeof(int), true, int32Value.ToString(CultureInfo.InvariantCulture), int32Value), + (typeof(string), typeof(long), true, int64Value.ToString(CultureInfo.InvariantCulture), int64Value), + (typeof(string), typeof(object), true, stringValue, stringValue), + (typeof(string), typeof(sbyte), true, sbyteValue.ToString(CultureInfo.InvariantCulture), sbyteValue), + (typeof(string), typeof(float), true, singleValue.ToString(CultureInfo.InvariantCulture), singleValue), + (typeof(string), typeof(string), true, stringValue, stringValue), + (typeof(string), typeof(TestEnum), true, enumValue.ToString(), enumValue), + ( + typeof(string), + typeof(TimeSpan), + true, + timeSpanValue.ToString("g", CultureInfo.InvariantCulture), + timeSpanValue + ), + (typeof(string), typeof(ushort), true, uint16Value.ToString(CultureInfo.InvariantCulture), uint16Value), + (typeof(string), typeof(uint), true, uint32Value.ToString(CultureInfo.InvariantCulture), uint32Value), + (typeof(string), typeof(ulong), true, uint64Value.ToString(CultureInfo.InvariantCulture), uint64Value), + (typeof(TestEnum), typeof(byte), true, enumValue, (byte)enumValue), + (typeof(TestEnum), typeof(decimal), true, enumValue, (decimal)enumValue), + (typeof(TestEnum), typeof(double), true, enumValue, (double)enumValue), + (typeof(TestEnum), typeof(short), true, enumValue, (short)enumValue), + (typeof(TestEnum), typeof(int), true, enumValue, (int)enumValue), + (typeof(TestEnum), typeof(long), true, enumValue, (long)enumValue), + (typeof(TestEnum), typeof(object), true, enumValue, enumValue), + (typeof(TestEnum), typeof(sbyte), true, enumValue, (sbyte)enumValue), + (typeof(TestEnum), typeof(float), true, enumValue, (float)enumValue), + (typeof(TestEnum), typeof(string), true, enumValue, enumValue.ToString()), + (typeof(TestEnum), typeof(TestEnum), true, enumValue, enumValue), + (typeof(TestEnum), typeof(ushort), true, enumValue, (ushort)enumValue), + (typeof(TestEnum), typeof(uint), true, enumValue, (uint)enumValue), + (typeof(TestEnum), typeof(ulong), true, enumValue, (ulong)enumValue), + (typeof(TimeOnly), typeof(object), true, timeOnlyValue, timeOnlyValue), + ( + typeof(TimeOnly), + typeof(string), + true, + timeOnlyValue, + timeOnlyValue.ToString("O", CultureInfo.InvariantCulture) + ), + (typeof(TimeOnly), typeof(TimeOnly), true, timeOnlyValue, timeOnlyValue), + (typeof(TimeSpan), typeof(object), true, timeSpanValue, timeSpanValue), + ( + typeof(TimeSpan), + typeof(string), + true, + timeSpanValue, + timeSpanValue.ToString("g", CultureInfo.InvariantCulture) + ), + (typeof(TimeSpan), typeof(TimeOnly), true, timeSpanValue, TimeOnly.FromTimeSpan(timeSpanValue)), + (typeof(TimeSpan), typeof(TimeSpan), true, timeSpanValue, timeSpanValue), + (typeof(ushort), typeof(bool), true, (ushort)1, true), + (typeof(ushort), typeof(byte), true, uint16Value, (byte)uint16Value), + (typeof(ushort), typeof(char), true, uint16Value, (char)uint16Value), + (typeof(ushort), typeof(decimal), true, uint16Value, (decimal)uint16Value), + (typeof(ushort), typeof(double), true, uint16Value, (double)uint16Value), + (typeof(ushort), typeof(short), true, uint16Value, (short)uint16Value), + (typeof(ushort), typeof(int), true, uint16Value, (int)uint16Value), + (typeof(ushort), typeof(long), true, uint16Value, (long)uint16Value), + (typeof(ushort), typeof(object), true, uint16Value, uint16Value), + (typeof(ushort), typeof(sbyte), true, uint16Value, (sbyte)uint16Value), + (typeof(ushort), typeof(float), true, uint16Value, (float)uint16Value), + (typeof(ushort), typeof(string), true, uint16Value, uint16Value.ToString(CultureInfo.InvariantCulture)), + (typeof(ushort), typeof(TestEnum), true, (ushort)enumValue, enumValue), + (typeof(ushort), typeof(ushort), true, uint16Value, uint16Value), + (typeof(ushort), typeof(uint), true, uint16Value, (uint)uint16Value), + (typeof(ushort), typeof(ulong), true, uint16Value, (ulong)uint16Value), + (typeof(uint), typeof(bool), true, (uint)1, true), + (typeof(uint), typeof(byte), true, uint32Value, (byte)uint32Value), + (typeof(uint), typeof(char), true, uint32Value, (char)uint32Value), + (typeof(uint), typeof(decimal), true, uint32Value, (decimal)uint32Value), + (typeof(uint), typeof(double), true, uint32Value, (double)uint32Value), + (typeof(uint), typeof(int), true, uint32Value, (int)uint32Value), + (typeof(uint), typeof(int), true, uint32Value, (int)uint32Value), + (typeof(uint), typeof(long), true, uint32Value, (long)uint32Value), + (typeof(uint), typeof(object), true, uint32Value, uint32Value), + (typeof(uint), typeof(sbyte), true, uint32Value, (sbyte)uint32Value), + (typeof(uint), typeof(float), true, uint32Value, (float)uint32Value), + (typeof(uint), typeof(string), true, uint32Value, uint32Value.ToString(CultureInfo.InvariantCulture)), + (typeof(uint), typeof(TestEnum), true, (uint)enumValue, enumValue), + (typeof(uint), typeof(ushort), true, uint32Value, (ushort)uint32Value), + (typeof(uint), typeof(uint), true, uint32Value, uint32Value), + (typeof(uint), typeof(ulong), true, uint32Value, (ulong)uint32Value), + (typeof(ulong), typeof(bool), true, (ulong)1, true), + (typeof(ulong), typeof(byte), true, uint64Value, (byte)uint64Value), + (typeof(ulong), typeof(char), true, uint64Value, (char)uint64Value), + (typeof(ulong), typeof(decimal), true, uint64Value, (decimal)uint64Value), + (typeof(ulong), typeof(double), true, uint64Value, (double)uint64Value), + (typeof(ulong), typeof(short), true, uint64Value, (short)uint64Value), + (typeof(ulong), typeof(int), true, uint64Value, (int)uint64Value), + (typeof(ulong), typeof(long), true, uint64Value, (long)uint64Value), + (typeof(ulong), typeof(object), true, uint64Value, uint64Value), + (typeof(ulong), typeof(sbyte), true, uint64Value, (sbyte)uint64Value), + (typeof(ulong), typeof(float), true, uint64Value, (float)uint64Value), + (typeof(ulong), typeof(string), true, uint64Value, uint64Value.ToString(CultureInfo.InvariantCulture)), + (typeof(ulong), typeof(TestEnum), true, (ulong)enumValue, enumValue), + (typeof(ulong), typeof(ushort), true, uint64Value, (ushort)uint64Value), + (typeof(ulong), typeof(uint), true, uint64Value, (uint)uint64Value), + (typeof(ulong), typeof(ulong), true, uint64Value, uint64Value), + (typeof(nuint), typeof(object), true, uintPtrValue, uintPtrValue), + (typeof(nuint), typeof(nuint), true, uintPtrValue, uintPtrValue), + (typeof(char), typeof(Guid), false, charValue, null), + (typeof(int), typeof(Guid), false, int32Value, null), + (typeof(DateTime), typeof(Guid), false, dateTimeValue, null), + (typeof(Guid), typeof(DateTime), false, guidValue, null), + (typeof(DateOnly), typeof(DateTime), false, dateOnlyValue, null), + (typeof(TimeOnly), typeof(TimeSpan), false, timeOnlyValue, null), + (typeof(DateOnly), typeof(Guid), false, dateOnlyValue, null), + (typeof(TimeOnly), typeof(Guid), false, timeOnlyValue, null), + ]; + + // @formatter:on + } + [Theory] [MemberData(nameof(GetConvertTestData))] public void CanConvert_NullableSourceType_ShouldDetermineIfConversionIsPossible( Type sourceType, Type targetType, - Boolean expectedCanConvert, - Object? sourceValue, - Object? expectedTargetValue + bool expectedCanConvert, + object? sourceValue, + object? expectedTargetValue ) { Assert.SkipUnless(sourceType.IsValueType, ""); @@ -43,9 +594,9 @@ public void CanConvert_NullableSourceType_ShouldDetermineIfConversionIsPossible( public void CanConvert_NullableTargetType_ShouldDetermineIfConversionIsPossible( Type sourceType, Type targetType, - Boolean expectedCanConvert, - Object? sourceValue, - Object? expectedTargetValue + bool expectedCanConvert, + object? sourceValue, + object? expectedTargetValue ) { Assert.SkipUnless(targetType.IsValueType, ""); @@ -67,199 +618,146 @@ public void CanConvert_NullableTargetType_ShouldDetermineIfConversionIsPossible( public void CanConvert_ShouldDetermineIfConversionIsPossible( Type sourceType, Type targetType, - Boolean expectedCanConvert, + bool expectedCanConvert, #pragma warning disable xUnit1026 // Theory methods should use all of their parameters #pragma warning disable RCS1163 // Unused parameter - Object? sourceValue, - Object? expectedTargetValue + object? sourceValue, + object? expectedTargetValue #pragma warning restore RCS1163 // Unused parameter #pragma warning restore xUnit1026 // Theory methods should use all of their parameters ) => - ValueConverter.CanConvert(sourceType, targetType) - .Should().Be( + ValueConverter + .CanConvert(sourceType, targetType) + .Should() + .Be( expectedCanConvert, $"{sourceType} should {(expectedCanConvert ? "" : "not ")}be convertible to {targetType}" ); [Fact] - public void ConvertValueToType_CharTargetType_StringWithLengthOneValue_ShouldGetFirstCharacter() + public void ConvertValueToTypeOfT_CharTargetType_StringWithLengthOneValue_ShouldGetFirstCharacter() { - var character = Generate.Single(); + var character = Generate.Single(); - ValueConverter.ConvertValueToType(character.ToString(), typeof(Char)) - .Should().Be(character); + ValueConverter.ConvertValueToType(character.ToString()).Should().Be(character); - ValueConverter.ConvertValueToType(character.ToString(), typeof(Char?)) - .Should().Be(character); + ValueConverter.ConvertValueToType(character.ToString()).Should().Be(character); } [Fact] - public void ConvertValueToType_CharTargetType_ValueIsStringWithLengthNotOne_ShouldThrow() + public void ConvertValueToTypeOfT_CharTargetType_ValueIsStringWithLengthNotOne_ShouldThrow() { - Invoking(() => ValueConverter.ConvertValueToType(String.Empty, typeof(Char))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(string.Empty)) + .Should() + .Throw() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly one " + - "character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly one " + + "character long." ); - Invoking(() => ValueConverter.ConvertValueToType(String.Empty, typeof(Char?))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(string.Empty)) + .Should() + .Throw() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char?)}. The string must be exactly one " + - "character long." + $"Could not convert the string '' to the type {typeof(char?)}. The string must be exactly one " + + "character long." ); - Invoking(() => ValueConverter.ConvertValueToType("ab", typeof(Char))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType("ab")) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly one " + - "character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly one " + + "character long." ); - Invoking(() => ValueConverter.ConvertValueToType("ab", typeof(Char?))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType("ab")) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char?)}. The string must be exactly one " + - "character long." + $"Could not convert the string 'ab' to the type {typeof(char?)}. The string must be exactly one " + + "character long." ); } - [Theory] - [InlineData("de-DE")] - [InlineData("fr-FR")] - [InlineData("en-US")] - public void ConvertValueToType_DateAndTimeStringValue_AmbiguousDate_ShouldNotDependOnTheCurrentCulture( - String cultureName - ) - { - // "03/04/2026" is the 4th of March under en-US and the 3rd of April under de-DE and fr-FR. Read with - // the invariant culture it is the 4th of March everywhere, so one database value can no longer decode - // into two different dates depending on the locale of the machine that runs the code. - var expectedDate = new DateOnly(2026, 3, 4); - - RunUnderCulture(cultureName, () => - { - ValueConverter.ConvertValueToType("03/04/2026") - .Should().Be(expectedDate); - - ValueConverter.ConvertValueToType("03/04/2026", typeof(DateOnly)) - .Should().Be(expectedDate); - }); - } - - [Theory] - [InlineData("de-DE")] - [InlineData("fr-FR")] - [InlineData("en-US")] - public void ConvertValueToType_DateAndTimeStringValue_ShouldRoundTripUnderAnyCulture(String cultureName) - { - // The converter writes these four types with the invariant culture, so it has to read them back the - // same way. It did not: under a culture whose decimal separator is a comma, a TimeSpan this library - // itself had written as "1:2:03:04.567" did not parse back at all, and the conversion threw. - var timeSpan = new TimeSpan(1, 2, 3, 4, 567); - var dateTimeOffset = new DateTimeOffset(2026, 3, 4, 14, 30, 0, TimeSpan.FromHours(2)); - var dateOnly = new DateOnly(2026, 3, 4); - var timeOnly = new TimeOnly(14, 30, 0); - - RunUnderCulture(cultureName, () => - { - AssertRoundTrips(timeSpan); - AssertRoundTrips(dateTimeOffset); - AssertRoundTrips(dateOnly); - AssertRoundTrips(timeOnly); - }); - - // Converts the value to its String representation and back, both through the converter itself, so the - // assertion is that the writing half and the reading half agree - not that either matches a literal. - static void AssertRoundTrips(TValue value) - { - var text = ValueConverter.ConvertValueToType(value); - - ValueConverter.ConvertValueToType(text) - .Should().Be(value, $"{typeof(TValue)} written as '{text}' should read back unchanged"); - - ValueConverter.ConvertValueToType(text, typeof(TValue)) - .Should().Be(value, $"{typeof(TValue)} written as '{text}' should read back unchanged"); - } - } - [Fact] - public void - ConvertValueToType_EnumTargetType_IntegerValueNotMatchingAnyEnumMemberValue_ShouldThrow() + public void ConvertValueToTypeOfT_EnumTargetType_IntegerValueNotMatchingAnyEnumMemberValue_ShouldThrow() { - Invoking(() => ValueConverter.ConvertValueToType(999, typeof(TestEnum))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(999)) + .Should() + .Throw() .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - Invoking(() => ValueConverter.ConvertValueToType(999, typeof(TestEnum?))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(999)) + .Should() + .Throw() .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum?)}. That value does not match any of the values of the enum's members.*" + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum?)}. That value does not match any of the values of the enum's members.*" ); } [Fact] - public void ConvertValueToType_EnumTargetType_ShouldConvertToEnumMember() + public void ConvertValueToTypeOfT_EnumTargetType_ShouldConvertToEnumMember() { var enumValue = Generate.Single(); - ValueConverter.ConvertValueToType((Int32)enumValue, typeof(TestEnum)) - .Should().Be(enumValue); + ValueConverter.ConvertValueToType((int)enumValue).Should().Be(enumValue); - ValueConverter.ConvertValueToType((Int32)enumValue, typeof(TestEnum?)) - .Should().Be(enumValue); + ValueConverter.ConvertValueToType((int)enumValue).Should().Be(enumValue); } [Fact] - public void - ConvertValueToType_EnumTargetType_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() + public void ConvertValueToTypeOfT_EnumTargetType_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() { - Invoking(() => ValueConverter.ConvertValueToType("NonExistent", typeof(TestEnum))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType("NonExistent")) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); - Invoking(() => ValueConverter.ConvertValueToType("NonExistent", typeof(TestEnum?))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType("NonExistent")) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum?)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum?)}. " + + "That string does not match any of the names of the enum's members.*" ); } [Fact] - public void ConvertValueToType_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() + public void ConvertValueToTypeOfT_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() { - Invoking(() => ValueConverter.ConvertValueToType(DBNull.Value, typeof(DateTime))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(DBNull.Value)) + .Should() + .Throw() .WithMessage( - $"Could not convert the value {{DBNull}} to the type {typeof(DateTime)}, because the " + - "type is non-nullable.*" + $"Could not convert the value {{DBNull}} to the type {typeof(DateTime)}, because the " + + "type is non-nullable.*" ); - Invoking(() => ValueConverter.ConvertValueToType(null, typeof(DateTime))) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(null)) + .Should() + .Throw() .WithMessage( - $"Could not convert the value {{null}} to the type {typeof(DateTime)}, because the type is " + - "non-nullable.*" + $"Could not convert the value {{null}} to the type {typeof(DateTime)}, because the type is " + + "non-nullable.*" ); } [Theory] [MemberData(nameof(GetConvertTestData))] - public void ConvertValueToType_NullableSourceType_ShouldConvertValueToTargetType( + public void ConvertValueToTypeOfT_NullableSourceType_ShouldConvertValueToTargetType( Type sourceType, Type targetType, - Boolean expectedCanConvert, - Object? sourceValue, - Object? expectedTargetValue + bool expectedCanConvert, + object? sourceValue, + object? expectedTargetValue ) { Assert.SkipUnless(sourceType.IsValueType, ""); @@ -267,7 +765,7 @@ public void ConvertValueToType_NullableSourceType_ShouldConvertValueToTargetType sourceType = typeof(Nullable<>).MakeGenericType(sourceType); sourceValue = Activator.CreateInstance(sourceType, sourceValue); - this.ConvertValueToType_ShouldConvertValueToType( + this.ConvertValueToTypeOfT_ShouldConvertValueToType( sourceType, targetType, expectedCanConvert, @@ -277,29 +775,25 @@ public void ConvertValueToType_NullableSourceType_ShouldConvertValueToTargetType } [Fact] - public void ConvertValueToType_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() + public void ConvertValueToTypeOfT_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() { - ValueConverter.ConvertValueToType(DBNull.Value, typeof(Object)) - .Should().BeNull(); + ValueConverter.ConvertValueToType(DBNull.Value).Should().BeNull(); - ValueConverter.ConvertValueToType(DBNull.Value, typeof(Int32?)) - .Should().BeNull(); + ValueConverter.ConvertValueToType(DBNull.Value).Should().BeNull(); - ValueConverter.ConvertValueToType(null, typeof(Object)) - .Should().BeNull(); + ValueConverter.ConvertValueToType(null).Should().BeNull(); - ValueConverter.ConvertValueToType(null, typeof(Int32?)) - .Should().BeNull(); + ValueConverter.ConvertValueToType(null).Should().BeNull(); } [Theory] [MemberData(nameof(GetConvertTestData))] - public void ConvertValueToType_NullableTargetType_ShouldConvertValueToTargetType( + public void ConvertValueToTypeOfT_NullableTargetType_ShouldConvertValueToTargetType( Type sourceType, Type targetType, - Boolean expectedCanConvert, - Object? sourceValue, - Object? expectedTargetValue + bool expectedCanConvert, + object? sourceValue, + object? expectedTargetValue ) { Assert.SkipUnless(targetType.IsValueType, ""); @@ -307,7 +801,7 @@ public void ConvertValueToType_NullableTargetType_ShouldConvertValueToTargetType targetType = typeof(Nullable<>).MakeGenericType(targetType); expectedTargetValue = Activator.CreateInstance(targetType, expectedTargetValue); - this.ConvertValueToType_ShouldConvertValueToType( + this.ConvertValueToTypeOfT_ShouldConvertValueToType( sourceType, targetType, expectedCanConvert, @@ -318,178 +812,258 @@ public void ConvertValueToType_NullableTargetType_ShouldConvertValueToTargetType [Theory] [MemberData(nameof(GetConvertTestData))] - public void ConvertValueToType_ShouldConvertValueToType( + public void ConvertValueToTypeOfT_ShouldConvertValueToType( Type _, Type targetType, - Boolean expectedCanConvert, - Object? sourceValue, - Object? expectedTargetValue + bool expectedCanConvert, + object? sourceValue, + object? expectedTargetValue ) { if (expectedCanConvert) { - var result = ValueConverter.ConvertValueToType(sourceValue, targetType); + var result = MaterializerFactoryHelper + .MakeValueConverterConvertValueToTypeMethod(targetType) + .Invoke(null, [sourceValue]); - if (result is Byte[] resultBytes && expectedTargetValue is Byte[] expectedTargetValueBytes) + if (result is byte[] resultBytes && expectedTargetValue is byte[] expectedTargetValueBytes) { resultBytes - .Should().BeEquivalentTo( + .Should() + .BeEquivalentTo( expectedTargetValueBytes, - $"{sourceValue.ToDebugString()} converted to {targetType} should be " + - $"{expectedTargetValue.ToDebugString()}" + $"{sourceValue.ToDebugString()} converted to {targetType} should be " + + $"{expectedTargetValue.ToDebugString()}" ); } else { result - .Should().Be( + .Should() + .Be( expectedTargetValue, - $"{sourceValue.ToDebugString()} converted to {targetType} should be " + - $"{expectedTargetValue.ToDebugString()}" + $"{sourceValue.ToDebugString()} converted to {targetType} should be " + + $"{expectedTargetValue.ToDebugString()}" ); } } else { - Invoking(() => ValueConverter.ConvertValueToType(sourceValue, targetType)) - .Should().Throw() - .WithMessage( - $"Could not convert the value {sourceValue.ToDebugString()} to the type {targetType}.*" - ); + Invoking(() => + MaterializerFactoryHelper + .MakeValueConverterConvertValueToTypeMethod(targetType) + .Invoke(null, [sourceValue]) + ) + .Should() + .Throw() + .WithInnerException() + .WithMessage($"Could not convert the value {sourceValue.ToDebugString()} to the type {targetType}.*"); } } [Fact] - public void ConvertValueToType_ValueCannotBeConvertedToTargetType_ShouldThrow() => - Invoking(() => ValueConverter.ConvertValueToType("NotADate", typeof(DateTime))) - .Should().Throw() + public void ConvertValueToTypeOfT_ValueCannotBeConvertedToTargetType_ShouldThrow() => + Invoking(() => ValueConverter.ConvertValueToType("NotADate")) + .Should() + .Throw() .WithMessage( - $"Could not convert the value 'NotADate' ({typeof(String)}) to the type {typeof(DateTime)}. See " + - "inner exception for details.*" + $"Could not convert the value 'NotADate' ({typeof(string)}) to the type {typeof(DateTime)}. See " + + "inner exception for details.*" ) .WithInnerException() .WithMessage("The string 'NotADate' was not recognized as a valid DateTime.*"); [Fact] - public void ConvertValueToTypeOfT_CharTargetType_StringWithLengthOneValue_ShouldGetFirstCharacter() + public void ConvertValueToType_CharTargetType_StringWithLengthOneValue_ShouldGetFirstCharacter() { - var character = Generate.Single(); + var character = Generate.Single(); - ValueConverter.ConvertValueToType(character.ToString()) - .Should().Be(character); + ValueConverter.ConvertValueToType(character.ToString(), typeof(char)).Should().Be(character); - ValueConverter.ConvertValueToType(character.ToString()) - .Should().Be(character); + ValueConverter.ConvertValueToType(character.ToString(), typeof(char?)).Should().Be(character); } [Fact] - public void ConvertValueToTypeOfT_CharTargetType_ValueIsStringWithLengthNotOne_ShouldThrow() + public void ConvertValueToType_CharTargetType_ValueIsStringWithLengthNotOne_ShouldThrow() { - Invoking(() => ValueConverter.ConvertValueToType(String.Empty)) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(string.Empty, typeof(char))) + .Should() + .Throw() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly one " + - "character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly one " + + "character long." ); - Invoking(() => ValueConverter.ConvertValueToType(String.Empty)) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(string.Empty, typeof(char?))) + .Should() + .Throw() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char?)}. The string must be exactly one " + - "character long." + $"Could not convert the string '' to the type {typeof(char?)}. The string must be exactly one " + + "character long." ); - Invoking(() => ValueConverter.ConvertValueToType("ab")) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType("ab", typeof(char))) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly one " + - "character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly one " + + "character long." ); - Invoking(() => ValueConverter.ConvertValueToType("ab")) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType("ab", typeof(char?))) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char?)}. The string must be exactly one " + - "character long." + $"Could not convert the string 'ab' to the type {typeof(char?)}. The string must be exactly one " + + "character long." ); } + [Theory] + [InlineData("de-DE")] + [InlineData("fr-FR")] + [InlineData("en-US")] + public void ConvertValueToType_DateAndTimeStringValue_AmbiguousDate_ShouldNotDependOnTheCurrentCulture( + string cultureName + ) + { + // "03/04/2026" is the 4th of March under en-US and the 3rd of April under de-DE and fr-FR. Read with + // the invariant culture it is the 4th of March everywhere, so one database value can no longer decode + // into two different dates depending on the locale of the machine that runs the code. + var expectedDate = new DateOnly(2026, 3, 4); + + RunUnderCulture( + cultureName, + () => + { + ValueConverter.ConvertValueToType("03/04/2026").Should().Be(expectedDate); + + ValueConverter.ConvertValueToType("03/04/2026", typeof(DateOnly)).Should().Be(expectedDate); + } + ); + } + + [Theory] + [InlineData("de-DE")] + [InlineData("fr-FR")] + [InlineData("en-US")] + public void ConvertValueToType_DateAndTimeStringValue_ShouldRoundTripUnderAnyCulture(string cultureName) + { + // The converter writes these four types with the invariant culture, so it has to read them back the + // same way. It did not: under a culture whose decimal separator is a comma, a TimeSpan this library + // itself had written as "1:2:03:04.567" did not parse back at all, and the conversion threw. + var timeSpan = new TimeSpan(1, 2, 3, 4, 567); + var dateTimeOffset = new DateTimeOffset(2026, 3, 4, 14, 30, 0, TimeSpan.FromHours(2)); + var dateOnly = new DateOnly(2026, 3, 4); + var timeOnly = new TimeOnly(14, 30, 0); + + RunUnderCulture( + cultureName, + () => + { + AssertRoundTrips(timeSpan); + AssertRoundTrips(dateTimeOffset); + AssertRoundTrips(dateOnly); + AssertRoundTrips(timeOnly); + } + ); + + // Converts the value to its String representation and back, both through the converter itself, so the + // assertion is that the writing half and the reading half agree - not that either matches a literal. + static void AssertRoundTrips(TValue value) + { + var text = ValueConverter.ConvertValueToType(value); + + ValueConverter + .ConvertValueToType(text) + .Should() + .Be(value, $"{typeof(TValue)} written as '{text}' should read back unchanged"); + + ValueConverter + .ConvertValueToType(text, typeof(TValue)) + .Should() + .Be(value, $"{typeof(TValue)} written as '{text}' should read back unchanged"); + } + } + [Fact] - public void - ConvertValueToTypeOfT_EnumTargetType_IntegerValueNotMatchingAnyEnumMemberValue_ShouldThrow() + public void ConvertValueToType_EnumTargetType_IntegerValueNotMatchingAnyEnumMemberValue_ShouldThrow() { - Invoking(() => ValueConverter.ConvertValueToType(999)) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(999, typeof(TestEnum))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - Invoking(() => ValueConverter.ConvertValueToType(999)) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(999, typeof(TestEnum?))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum?)}. That value does not match any of the values of the enum's members.*" + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum?)}. That value does not match any of the values of the enum's members.*" ); } [Fact] - public void ConvertValueToTypeOfT_EnumTargetType_ShouldConvertToEnumMember() + public void ConvertValueToType_EnumTargetType_ShouldConvertToEnumMember() { var enumValue = Generate.Single(); - ValueConverter.ConvertValueToType((Int32)enumValue) - .Should().Be(enumValue); + ValueConverter.ConvertValueToType((int)enumValue, typeof(TestEnum)).Should().Be(enumValue); - ValueConverter.ConvertValueToType((Int32)enumValue) - .Should().Be(enumValue); + ValueConverter.ConvertValueToType((int)enumValue, typeof(TestEnum?)).Should().Be(enumValue); } [Fact] - public void - ConvertValueToTypeOfT_EnumTargetType_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() + public void ConvertValueToType_EnumTargetType_StringValueNotMatchingAnyEnumMemberName_ShouldThrow() { - Invoking(() => ValueConverter.ConvertValueToType("NonExistent")) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType("NonExistent", typeof(TestEnum))) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. " + + "That string does not match any of the names of the enum's members.*" ); - Invoking(() => ValueConverter.ConvertValueToType("NonExistent")) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType("NonExistent", typeof(TestEnum?))) + .Should() + .Throw() .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum?)}. " + - "That string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum?)}. " + + "That string does not match any of the names of the enum's members.*" ); } [Fact] - public void ConvertValueToTypeOfT_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() + public void ConvertValueToType_NonNullableTargetType_NullOrDBNullValue_ShouldThrow() { - Invoking(() => ValueConverter.ConvertValueToType(DBNull.Value)) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(DBNull.Value, typeof(DateTime))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value {{DBNull}} to the type {typeof(DateTime)}, because the " + - "type is non-nullable.*" + $"Could not convert the value {{DBNull}} to the type {typeof(DateTime)}, because the " + + "type is non-nullable.*" ); - Invoking(() => ValueConverter.ConvertValueToType(null)) - .Should().Throw() + Invoking(() => ValueConverter.ConvertValueToType(null, typeof(DateTime))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value {{null}} to the type {typeof(DateTime)}, because the type is " + - "non-nullable.*" + $"Could not convert the value {{null}} to the type {typeof(DateTime)}, because the type is " + + "non-nullable.*" ); } [Theory] [MemberData(nameof(GetConvertTestData))] - public void ConvertValueToTypeOfT_NullableSourceType_ShouldConvertValueToTargetType( + public void ConvertValueToType_NullableSourceType_ShouldConvertValueToTargetType( Type sourceType, Type targetType, - Boolean expectedCanConvert, - Object? sourceValue, - Object? expectedTargetValue + bool expectedCanConvert, + object? sourceValue, + object? expectedTargetValue ) { Assert.SkipUnless(sourceType.IsValueType, ""); @@ -497,7 +1071,7 @@ public void ConvertValueToTypeOfT_NullableSourceType_ShouldConvertValueToTargetT sourceType = typeof(Nullable<>).MakeGenericType(sourceType); sourceValue = Activator.CreateInstance(sourceType, sourceValue); - this.ConvertValueToTypeOfT_ShouldConvertValueToType( + this.ConvertValueToType_ShouldConvertValueToType( sourceType, targetType, expectedCanConvert, @@ -507,29 +1081,25 @@ public void ConvertValueToTypeOfT_NullableSourceType_ShouldConvertValueToTargetT } [Fact] - public void ConvertValueToTypeOfT_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() + public void ConvertValueToType_NullableTargetType_NullOrDBNullValue_ShouldReturnNull() { - ValueConverter.ConvertValueToType(DBNull.Value) - .Should().BeNull(); + ValueConverter.ConvertValueToType(DBNull.Value, typeof(object)).Should().BeNull(); - ValueConverter.ConvertValueToType(DBNull.Value) - .Should().BeNull(); + ValueConverter.ConvertValueToType(DBNull.Value, typeof(int?)).Should().BeNull(); - ValueConverter.ConvertValueToType(null) - .Should().BeNull(); + ValueConverter.ConvertValueToType(null, typeof(object)).Should().BeNull(); - ValueConverter.ConvertValueToType(null) - .Should().BeNull(); + ValueConverter.ConvertValueToType(null, typeof(int?)).Should().BeNull(); } [Theory] [MemberData(nameof(GetConvertTestData))] - public void ConvertValueToTypeOfT_NullableTargetType_ShouldConvertValueToTargetType( + public void ConvertValueToType_NullableTargetType_ShouldConvertValueToTargetType( Type sourceType, Type targetType, - Boolean expectedCanConvert, - Object? sourceValue, - Object? expectedTargetValue + bool expectedCanConvert, + object? sourceValue, + object? expectedTargetValue ) { Assert.SkipUnless(targetType.IsValueType, ""); @@ -537,7 +1107,7 @@ public void ConvertValueToTypeOfT_NullableTargetType_ShouldConvertValueToTargetT targetType = typeof(Nullable<>).MakeGenericType(targetType); expectedTargetValue = Activator.CreateInstance(targetType, expectedTargetValue); - this.ConvertValueToTypeOfT_ShouldConvertValueToType( + this.ConvertValueToType_ShouldConvertValueToType( sourceType, targetType, expectedCanConvert, @@ -548,59 +1118,56 @@ public void ConvertValueToTypeOfT_NullableTargetType_ShouldConvertValueToTargetT [Theory] [MemberData(nameof(GetConvertTestData))] - public void ConvertValueToTypeOfT_ShouldConvertValueToType( + public void ConvertValueToType_ShouldConvertValueToType( Type _, Type targetType, - Boolean expectedCanConvert, - Object? sourceValue, - Object? expectedTargetValue + bool expectedCanConvert, + object? sourceValue, + object? expectedTargetValue ) { if (expectedCanConvert) { - var result = MaterializerFactoryHelper.MakeValueConverterConvertValueToTypeMethod(targetType) - .Invoke(null, [sourceValue]); + var result = ValueConverter.ConvertValueToType(sourceValue, targetType); - if (result is Byte[] resultBytes && expectedTargetValue is Byte[] expectedTargetValueBytes) + if (result is byte[] resultBytes && expectedTargetValue is byte[] expectedTargetValueBytes) { resultBytes - .Should().BeEquivalentTo( + .Should() + .BeEquivalentTo( expectedTargetValueBytes, - $"{sourceValue.ToDebugString()} converted to {targetType} should be " + - $"{expectedTargetValue.ToDebugString()}" + $"{sourceValue.ToDebugString()} converted to {targetType} should be " + + $"{expectedTargetValue.ToDebugString()}" ); } else { result - .Should().Be( + .Should() + .Be( expectedTargetValue, - $"{sourceValue.ToDebugString()} converted to {targetType} should be " + - $"{expectedTargetValue.ToDebugString()}" + $"{sourceValue.ToDebugString()} converted to {targetType} should be " + + $"{expectedTargetValue.ToDebugString()}" ); } } else { - Invoking(() => - MaterializerFactoryHelper.MakeValueConverterConvertValueToTypeMethod(targetType) - .Invoke(null, [sourceValue]) - ) - .Should().Throw() - .WithInnerException() - .WithMessage( - $"Could not convert the value {sourceValue.ToDebugString()} to the type {targetType}.*" - ); + Invoking(() => ValueConverter.ConvertValueToType(sourceValue, targetType)) + .Should() + .Throw() + .WithMessage($"Could not convert the value {sourceValue.ToDebugString()} to the type {targetType}.*"); } } [Fact] - public void ConvertValueToTypeOfT_ValueCannotBeConvertedToTargetType_ShouldThrow() => - Invoking(() => ValueConverter.ConvertValueToType("NotADate")) - .Should().Throw() + public void ConvertValueToType_ValueCannotBeConvertedToTargetType_ShouldThrow() => + Invoking(() => ValueConverter.ConvertValueToType("NotADate", typeof(DateTime))) + .Should() + .Throw() .WithMessage( - $"Could not convert the value 'NotADate' ({typeof(String)}) to the type {typeof(DateTime)}. See " + - "inner exception for details.*" + $"Could not convert the value 'NotADate' ({typeof(string)}) to the type {typeof(DateTime)}. See " + + "inner exception for details.*" ) .WithInnerException() .WithMessage("The string 'NotADate' was not recognized as a valid DateTime.*"); @@ -608,13 +1175,13 @@ public void ConvertValueToTypeOfT_ValueCannotBeConvertedToTargetType_ShouldThrow [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => ValueConverter.CanConvert(typeof(Int16), typeof(Int32))); - ArgumentNullGuardVerifier.Verify(() => ValueConverter.ConvertValueToType(1, typeof(Int32))); + ArgumentNullGuardVerifier.Verify(() => ValueConverter.CanConvert(typeof(short), typeof(int))); + ArgumentNullGuardVerifier.Verify(() => ValueConverter.ConvertValueToType(1, typeof(int))); } /// /// Runs with the current culture set to , - /// and restores the previous culture afterwards. + /// and restores the previous culture afterward. /// /// /// pins every test to en-US, and en-US is exactly the culture under which @@ -624,16 +1191,16 @@ public void ShouldGuardAgainstNullArguments() /// /// The name of the culture to run the assertions under. /// The assertions to run. - private static void RunUnderCulture(String cultureName, Action assertions) + private static void RunUnderCulture(string cultureName, Action assertions) { var culture = new CultureInfo(cultureName); // Without ICU, every culture collapses into the invariant one and the test would pass while proving // nothing. de-DE and fr-FR both separate decimals with a comma; the invariant culture uses a dot. Assert.SkipWhen( - cultureName != "en-US" && - culture.NumberFormat.NumberDecimalSeparator == - CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator, + cultureName != "en-US" + && culture.NumberFormat.NumberDecimalSeparator + == CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator, $"Globalization is in invariant mode, so '{cultureName}' is not a real culture here." ); @@ -650,322 +1217,4 @@ private static void RunUnderCulture(String cultureName, Action assertions) CultureInfo.CurrentCulture = Thread.CurrentThread.CurrentCulture = previousCulture; } } - - public static IEnumerable<( - Type SourceType, - Type TargetType, - Boolean ExpectedCanConvert, - Object SourceValue, - Object ExpectedTargetValue - )> - GetConvertTestData() - { - var faker = new Faker(); - - // All numeric values are kept within the range 0-127 so they are convertible to the smallest target type - // (SByte) without overflow. - var byteValue = faker.Random.Byte(0, 127); - var charValue = faker.Random.Char('A', 'Z'); - var dateOnlyValue = faker.Date.PastDateOnly(); - var dateTimeValue = faker.Date.Past(); - var dateTimeOffsetValue = faker.Date.PastOffset(); - var decimalValue = faker.Random.Decimal(0, 127); - var doubleValue = faker.Random.Double(0, 127); - var guidValue = faker.Random.Guid(); - var int16Value = faker.Random.Short(0, 127); - var int32Value = faker.Random.Int(0, 127); - var int64Value = faker.Random.Long(0, 127); - var intPtrValue = (IntPtr)faker.Random.Int(0, 127); - var sbyteValue = faker.Random.SByte(0); - var singleValue = faker.Random.Float(0, 127); - var stringValue = faker.Lorem.Sentence(); - var uint16Value = faker.Random.UShort(0, 127); - var uint32Value = faker.Random.UInt(0, 127); - var uint64Value = faker.Random.ULong(0, 127); - var timeSpanValue = faker.Date.Timespan(TimeSpan.FromHours(23)); - var timeOnlyValue = faker.Date.RecentTimeOnly(); - var uintPtrValue = (UIntPtr)faker.Random.Int(0, 127); - var enumValue = faker.Random.Enum(); - - // @formatter:off - - return new List<( - Type SourceType, - Type TargetType, - Boolean ExpectedCanConvert, - Object? SourceValue, - Object? ExpectedTargetValue - )> - { - (typeof(Boolean), typeof(Boolean), true, true, true), - (typeof(Boolean), typeof(Byte), true, true, (Byte)1), - (typeof(Boolean), typeof(Decimal), true, true, (Decimal)1), - (typeof(Boolean), typeof(Double), true, true, (Double)1), - (typeof(Boolean), typeof(Int16), true, true, (Int16)1), - (typeof(Boolean), typeof(Int32), true, true, 1), - (typeof(Boolean), typeof(Int64), true, true, (Int64)1), - (typeof(Boolean), typeof(Object), true, true, true), - (typeof(Boolean), typeof(SByte), true, true, (SByte)1), - (typeof(Boolean), typeof(Single), true, true, (Single)1), - (typeof(Boolean), typeof(String), true, true, "True"), - (typeof(Boolean), typeof(UInt16), true, true, (UInt16)1), - (typeof(Boolean), typeof(UInt32), true, true, (UInt32)1), - (typeof(Boolean), typeof(UInt64), true, true, (UInt64)1), - (typeof(Byte), typeof(Boolean), true, (Byte)1, true), - (typeof(Byte), typeof(Byte), true, byteValue, byteValue), - (typeof(Byte), typeof(Char), true, byteValue, (Char)byteValue), - (typeof(Byte), typeof(Decimal), true, byteValue, (Decimal)byteValue), - (typeof(Byte), typeof(Double), true, byteValue, (Double)byteValue), - (typeof(Byte), typeof(Int16), true, byteValue, (Int16)byteValue), - (typeof(Byte), typeof(Int32), true, byteValue, (Int32)byteValue), - (typeof(Byte), typeof(Int64), true, byteValue, (Int64)byteValue), - (typeof(Byte), typeof(Object), true, byteValue, byteValue), - (typeof(Byte), typeof(SByte), true, byteValue, (SByte)byteValue), - (typeof(Byte), typeof(Single), true, byteValue, (Single)byteValue), - (typeof(Byte), typeof(String), true, byteValue, byteValue.ToString(CultureInfo.InvariantCulture)), - (typeof(Byte), typeof(TestEnum), true, (Byte)enumValue, enumValue), - (typeof(Byte), typeof(UInt16), true, byteValue, (UInt16)byteValue), - (typeof(Byte), typeof(UInt32), true, byteValue, (UInt32)byteValue), - (typeof(Byte), typeof(UInt64), true, byteValue, (UInt64)byteValue), - (typeof(Byte[]), typeof(Guid), true, guidValue.ToByteArray(), guidValue), - (typeof(Char), typeof(Byte), true, charValue, (Byte)charValue), - (typeof(Char), typeof(Char), true, charValue, charValue), - (typeof(Char), typeof(Int16), true, charValue, (Int16)charValue), - (typeof(Char), typeof(Int32), true, charValue, (Int32)charValue), - (typeof(Char), typeof(Int64), true, charValue, (Int64)charValue), - (typeof(Char), typeof(Object), true, charValue, charValue), - (typeof(Char), typeof(SByte), true, charValue, (SByte)charValue), - (typeof(Char), typeof(String), true, charValue, charValue.ToString(CultureInfo.InvariantCulture)), - (typeof(Char), typeof(UInt16), true, charValue, (UInt16)charValue), - (typeof(Char), typeof(UInt32), true, charValue, (UInt32)charValue), - (typeof(Char), typeof(UInt64), true, charValue, (UInt64)charValue), - (typeof(DateOnly), typeof(DateOnly), true, dateOnlyValue, dateOnlyValue), - (typeof(DateOnly), typeof(Object), true, dateOnlyValue, dateOnlyValue), - (typeof(DateOnly), typeof(String), true, dateOnlyValue, dateOnlyValue.ToString("O", CultureInfo.InvariantCulture)), - (typeof(DateTime), typeof(DateOnly), true, dateOnlyValue.ToDateTime(TimeOnly.MinValue), dateOnlyValue), - (typeof(DateTime), typeof(DateTime), true, dateTimeValue, dateTimeValue), - (typeof(DateTime), typeof(Object), true, dateTimeValue, dateTimeValue), - (typeof(DateTime), typeof(String), true, dateTimeValue, dateTimeValue.ToString("O", CultureInfo.InvariantCulture)), - (typeof(DateTimeOffset), typeof(DateTimeOffset), true, dateTimeOffsetValue, dateTimeOffsetValue), - (typeof(DateTimeOffset), typeof(Object), true, dateTimeOffsetValue, dateTimeOffsetValue), - (typeof(DateTimeOffset), typeof(String), true, dateTimeOffsetValue, dateTimeOffsetValue.ToString("O", CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(Boolean), true, 1M, true), - (typeof(Decimal), typeof(Byte), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Byte), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(Decimal), true, decimalValue, decimalValue), - (typeof(Decimal), typeof(Double), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Double), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(Int16), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Int16), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(Int32), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Int32), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(Int64), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Int64), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(Object), true, decimalValue, decimalValue), - (typeof(Decimal), typeof(SByte), true, decimalValue, Convert.ChangeType(decimalValue, typeof(SByte), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(Single), true, decimalValue, Convert.ChangeType(decimalValue, typeof(Single), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(String), true, decimalValue, decimalValue.ToString(CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(TestEnum), true, (Decimal)enumValue, enumValue), - (typeof(Decimal), typeof(UInt16), true, decimalValue, Convert.ChangeType(decimalValue, typeof(UInt16), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(UInt32), true, decimalValue, Convert.ChangeType(decimalValue, typeof(UInt32), CultureInfo.InvariantCulture)), - (typeof(Decimal), typeof(UInt64), true, decimalValue, Convert.ChangeType(decimalValue, typeof(UInt64), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(Boolean), true, 1.0, true), - (typeof(Double), typeof(Byte), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Byte), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(Decimal), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Decimal), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(Double), true, doubleValue, doubleValue), - (typeof(Double), typeof(Int16), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Int16), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(Int32), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Int32), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(Int64), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Int64), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(Object), true, doubleValue, doubleValue), - (typeof(Double), typeof(SByte), true, doubleValue, Convert.ChangeType(doubleValue, typeof(SByte), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(Single), true, doubleValue, Convert.ChangeType(doubleValue, typeof(Single), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(String), true, doubleValue, doubleValue.ToString(CultureInfo.InvariantCulture)), - (typeof(Double), typeof(TestEnum), true, (Double)enumValue, enumValue), - (typeof(Double), typeof(UInt16), true, doubleValue, Convert.ChangeType(doubleValue, typeof(UInt16), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(UInt32), true, doubleValue, Convert.ChangeType(doubleValue, typeof(UInt32), CultureInfo.InvariantCulture)), - (typeof(Double), typeof(UInt64), true, doubleValue, Convert.ChangeType(doubleValue, typeof(UInt64), CultureInfo.InvariantCulture)), - (typeof(Guid), typeof(Byte[]), true, guidValue, guidValue.ToByteArray()), - (typeof(Guid), typeof(Guid), true, guidValue, guidValue), - (typeof(Guid), typeof(Object), true, guidValue, guidValue), - (typeof(Guid), typeof(String), true, guidValue, guidValue.ToString("D")), - (typeof(Int16), typeof(Boolean), true, (Int16)1, true), - (typeof(Int16), typeof(Byte), true, int16Value, (Byte) int16Value), - (typeof(Int16), typeof(Char), true, int16Value, (Char) int16Value), - (typeof(Int16), typeof(Decimal), true, int16Value, (Decimal) int16Value), - (typeof(Int16), typeof(Double), true, int16Value, (Double)int16Value), - (typeof(Int16), typeof(Int16), true, int16Value, int16Value), - (typeof(Int16), typeof(Int32), true, int16Value, (Int32)int16Value), - (typeof(Int16), typeof(Int64), true, int16Value, (Int64)int16Value), - (typeof(Int16), typeof(Object), true, int16Value, int16Value), - (typeof(Int16), typeof(SByte), true, int16Value, (SByte)int16Value), - (typeof(Int16), typeof(Single), true, int16Value, (Single)int16Value), - (typeof(Int16), typeof(String), true, int16Value, int16Value.ToString(CultureInfo.InvariantCulture)), - (typeof(Int16), typeof(TestEnum), true, (Int16)enumValue, enumValue), - (typeof(Int16), typeof(UInt16), true, int16Value, (UInt16)int16Value), - (typeof(Int16), typeof(UInt32), true, int16Value, (UInt32)int16Value), - (typeof(Int16), typeof(UInt64), true, int16Value, (UInt64)int16Value), - (typeof(Int32), typeof(Boolean), true, 1, true), - (typeof(Int32), typeof(Byte), true, int32Value, (Byte)int32Value), - (typeof(Int32), typeof(Char), true, int32Value, (Char) int32Value), - (typeof(Int32), typeof(Decimal), true, int32Value, (Decimal)int32Value), - (typeof(Int32), typeof(Double), true, int32Value, (Double)int32Value), - (typeof(Int32), typeof(Int16), true, int32Value, (Int16)int32Value), - (typeof(Int32), typeof(Int32), true, int32Value, int32Value), - (typeof(Int32), typeof(Int64), true, int32Value, (Int64)int32Value), - (typeof(Int32), typeof(Object), true, int32Value, int32Value), - (typeof(Int32), typeof(SByte), true, int32Value, (SByte)int32Value), - (typeof(Int32), typeof(Single), true, int32Value, (Single)int32Value), - (typeof(Int32), typeof(String), true, int32Value, int32Value.ToString(CultureInfo.InvariantCulture)), - (typeof(Int32), typeof(TestEnum), true, (Int32)enumValue, enumValue), - (typeof(Int32), typeof(UInt16), true, int32Value, (UInt16)int32Value), - (typeof(Int32), typeof(UInt32), true, int32Value, (UInt32)int32Value), - (typeof(Int32), typeof(UInt64), true, int32Value, (UInt64)int32Value), - (typeof(Int64), typeof(Boolean), true, (Int64)1, true), - (typeof(Int64), typeof(Byte), true, int64Value, (Byte) int64Value), - (typeof(Int64), typeof(Char), true, int64Value, (Char) int64Value), - (typeof(Int64), typeof(Decimal), true, int64Value, (Decimal) int64Value), - (typeof(Int64), typeof(Double), true, int64Value, (Double)int64Value), - (typeof(Int64), typeof(Int16), true, int64Value, (Int16)int64Value), - (typeof(Int64), typeof(Int32), true, int64Value, (Int32)int64Value), - (typeof(Int64), typeof(Int64), true, int64Value, int64Value), - (typeof(Int64), typeof(Object), true, int64Value, int64Value), - (typeof(Int64), typeof(SByte), true, int64Value, (SByte)int64Value), - (typeof(Int64), typeof(Single), true, int64Value, (Single)int64Value), - (typeof(Int64), typeof(String), true, int64Value, int64Value.ToString(CultureInfo.InvariantCulture)), - (typeof(Int64), typeof(TestEnum), true, (Int64)enumValue, enumValue), - (typeof(Int64), typeof(UInt16), true, int64Value, (UInt16)int64Value), - (typeof(Int64), typeof(UInt32), true, int64Value, (UInt32)int64Value), - (typeof(Int64), typeof(UInt64), true, int64Value, (UInt64)int64Value), - (typeof(IntPtr), typeof(IntPtr), true, intPtrValue, intPtrValue), - (typeof(IntPtr), typeof(Object), true, intPtrValue, intPtrValue), - (typeof(SByte), typeof(Boolean), true, (SByte)1, true), - (typeof(SByte), typeof(Byte), true, sbyteValue, (Byte)sbyteValue), - (typeof(SByte), typeof(Char), true, sbyteValue, (Char) sbyteValue), - (typeof(SByte), typeof(Decimal), true, sbyteValue, (Decimal)sbyteValue), - (typeof(SByte), typeof(Double), true, sbyteValue, (Double)sbyteValue), - (typeof(SByte), typeof(Int16), true, sbyteValue, (Int16)sbyteValue), - (typeof(SByte), typeof(Int32), true, sbyteValue, (Int32)sbyteValue), - (typeof(SByte), typeof(Int64), true, sbyteValue, (Int64)sbyteValue), - (typeof(SByte), typeof(Object), true, sbyteValue, sbyteValue), - (typeof(SByte), typeof(SByte), true, sbyteValue, sbyteValue), - (typeof(SByte), typeof(Single), true, sbyteValue, (Single)sbyteValue), - (typeof(SByte), typeof(String), true, sbyteValue, sbyteValue.ToString(CultureInfo.InvariantCulture)), - (typeof(SByte), typeof(TestEnum), true, (SByte)enumValue, enumValue), - (typeof(SByte), typeof(UInt16), true, sbyteValue, (UInt16)sbyteValue), - (typeof(SByte), typeof(UInt32), true, sbyteValue, (UInt32)sbyteValue), - (typeof(SByte), typeof(UInt64), true, sbyteValue, (UInt64)sbyteValue), - (typeof(Single), typeof(Boolean), true, (Single)1, true), - (typeof(Single), typeof(Byte), true, singleValue, Convert.ChangeType(singleValue, typeof(Byte), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(Decimal), true, singleValue, Convert.ChangeType(singleValue, typeof(Decimal), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(Double), true, singleValue, Convert.ChangeType(singleValue, typeof(Double), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(Int16), true, singleValue, Convert.ChangeType(singleValue, typeof(Int16), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(Int32), true, singleValue, Convert.ChangeType(singleValue, typeof(Int32), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(Int64), true, singleValue, Convert.ChangeType(singleValue, typeof(Int64), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(Object), true, singleValue, singleValue), - (typeof(Single), typeof(SByte), true, singleValue, Convert.ChangeType(singleValue, typeof(SByte), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(Single), true, singleValue, Convert.ChangeType(singleValue, typeof(Single), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(String), true, singleValue, singleValue.ToString(CultureInfo.InvariantCulture)), - (typeof(Single), typeof(TestEnum), true, (Single)enumValue, enumValue), - (typeof(Single), typeof(UInt16), true, singleValue, Convert.ChangeType(singleValue, typeof(UInt16), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(UInt32), true, singleValue, Convert.ChangeType(singleValue, typeof(UInt32), CultureInfo.InvariantCulture)), - (typeof(Single), typeof(UInt64), true, singleValue, Convert.ChangeType(singleValue, typeof(UInt64), CultureInfo.InvariantCulture)), - (typeof(String), typeof(Boolean), true, "True", true), - (typeof(String), typeof(Byte), true, byteValue.ToString(CultureInfo.InvariantCulture), byteValue), - (typeof(String), typeof(Char), true, charValue.ToString(CultureInfo.InvariantCulture), charValue), - (typeof(String), typeof(DateOnly), true, dateOnlyValue.ToString("O", CultureInfo.InvariantCulture), dateOnlyValue), - (typeof(String), typeof(DateTime), true, dateTimeValue.ToString("O", CultureInfo.InvariantCulture), dateTimeValue), - (typeof(String), typeof(DateTimeOffset), true, dateTimeOffsetValue.ToString("O", CultureInfo.InvariantCulture), dateTimeOffsetValue), - (typeof(String), typeof(Decimal), true, decimalValue.ToString(CultureInfo.InvariantCulture), decimalValue), - (typeof(String), typeof(Double), true, doubleValue.ToString(CultureInfo.InvariantCulture), doubleValue), - (typeof(String), typeof(Guid), true, guidValue.ToString("D"), guidValue), - (typeof(String), typeof(Int16), true, int16Value.ToString(CultureInfo.InvariantCulture), int16Value), - (typeof(String), typeof(Int32), true, int32Value.ToString(CultureInfo.InvariantCulture), int32Value), - (typeof(String), typeof(Int64), true, int64Value.ToString(CultureInfo.InvariantCulture), int64Value), - (typeof(String), typeof(Object), true, stringValue, stringValue), - (typeof(String), typeof(SByte), true, sbyteValue.ToString(CultureInfo.InvariantCulture), sbyteValue), - (typeof(String), typeof(Single), true, singleValue.ToString(CultureInfo.InvariantCulture), singleValue), - (typeof(String), typeof(String), true, stringValue, stringValue), - (typeof(String), typeof(TestEnum), true, enumValue.ToString(), enumValue), - (typeof(String), typeof(TimeSpan), true, timeSpanValue.ToString("g", CultureInfo.InvariantCulture), timeSpanValue), - (typeof(String), typeof(UInt16), true, uint16Value.ToString(CultureInfo.InvariantCulture), uint16Value), - (typeof(String), typeof(UInt32), true, uint32Value.ToString(CultureInfo.InvariantCulture), uint32Value), - (typeof(String), typeof(UInt64), true, uint64Value.ToString(CultureInfo.InvariantCulture), uint64Value), - (typeof(TestEnum), typeof(Byte), true, enumValue, (Byte)enumValue), - (typeof(TestEnum), typeof(Decimal), true, enumValue, (Decimal)enumValue), - (typeof(TestEnum), typeof(Double), true, enumValue, (Double)enumValue), - (typeof(TestEnum), typeof(Int16), true, enumValue, (Int16)enumValue), - (typeof(TestEnum), typeof(Int32), true, enumValue, (Int32)enumValue), - (typeof(TestEnum), typeof(Int64), true, enumValue, (Int64)enumValue), - (typeof(TestEnum), typeof(Object), true, enumValue, enumValue), - (typeof(TestEnum), typeof(SByte), true, enumValue, (SByte)enumValue), - (typeof(TestEnum), typeof(Single), true, enumValue, (Single)enumValue), - (typeof(TestEnum), typeof(String), true, enumValue, enumValue.ToString()), - (typeof(TestEnum), typeof(TestEnum), true, enumValue, enumValue), - (typeof(TestEnum), typeof(UInt16), true, enumValue, (UInt16)enumValue), - (typeof(TestEnum), typeof(UInt32), true, enumValue, (UInt32)enumValue), - (typeof(TestEnum), typeof(UInt64), true, enumValue, (UInt64)enumValue), - (typeof(TimeOnly), typeof(Object), true, timeOnlyValue, timeOnlyValue), - (typeof(TimeOnly), typeof(String), true, timeOnlyValue, timeOnlyValue.ToString("O", CultureInfo.InvariantCulture)), - (typeof(TimeOnly), typeof(TimeOnly), true, timeOnlyValue, timeOnlyValue), - (typeof(TimeSpan), typeof(Object), true, timeSpanValue, timeSpanValue), - (typeof(TimeSpan), typeof(String), true, timeSpanValue, timeSpanValue.ToString("g", CultureInfo.InvariantCulture)), - (typeof(TimeSpan), typeof(TimeOnly), true, timeSpanValue, TimeOnly.FromTimeSpan(timeSpanValue)), - (typeof(TimeSpan), typeof(TimeSpan), true, timeSpanValue, timeSpanValue), - (typeof(UInt16), typeof(Boolean), true, (UInt16)1, true), - (typeof(UInt16), typeof(Byte), true, uint16Value, (Byte) uint16Value), - (typeof(UInt16), typeof(Char), true, uint16Value, (Char) uint16Value), - (typeof(UInt16), typeof(Decimal), true, uint16Value, (Decimal) uint16Value), - (typeof(UInt16), typeof(Double), true, uint16Value, (Double)uint16Value), - (typeof(UInt16), typeof(Int16), true, uint16Value, (Int16)uint16Value), - (typeof(UInt16), typeof(Int32), true, uint16Value, (Int32)uint16Value), - (typeof(UInt16), typeof(Int64), true, uint16Value, (Int64)uint16Value), - (typeof(UInt16), typeof(Object), true, uint16Value, uint16Value), - (typeof(UInt16), typeof(SByte), true, uint16Value, (SByte)uint16Value), - (typeof(UInt16), typeof(Single), true, uint16Value, (Single)uint16Value), - (typeof(UInt16), typeof(String), true, uint16Value, uint16Value.ToString(CultureInfo.InvariantCulture)), - (typeof(UInt16), typeof(TestEnum), true, (UInt16)enumValue, enumValue), - (typeof(UInt16), typeof(UInt16), true, uint16Value, uint16Value), - (typeof(UInt16), typeof(UInt32), true, uint16Value, (UInt32)uint16Value), - (typeof(UInt16), typeof(UInt64), true, uint16Value, (UInt64)uint16Value), - (typeof(UInt32), typeof(Boolean), true, (UInt32)1, true), - (typeof(UInt32), typeof(Byte), true, uint32Value, (Byte) uint32Value), - (typeof(UInt32), typeof(Char), true, uint32Value, (Char) uint32Value), - (typeof(UInt32), typeof(Decimal), true, uint32Value, (Decimal) uint32Value), - (typeof(UInt32), typeof(Double), true, uint32Value, (Double)uint32Value), - (typeof(UInt32), typeof(Int32), true, uint32Value, (Int32)uint32Value), - (typeof(UInt32), typeof(Int32), true, uint32Value, (Int32)uint32Value), - (typeof(UInt32), typeof(Int64), true, uint32Value, (Int64)uint32Value), - (typeof(UInt32), typeof(Object), true, uint32Value, uint32Value), - (typeof(UInt32), typeof(SByte), true, uint32Value, (SByte)uint32Value), - (typeof(UInt32), typeof(Single), true, uint32Value, (Single)uint32Value), - (typeof(UInt32), typeof(String), true, uint32Value, uint32Value.ToString(CultureInfo.InvariantCulture)), - (typeof(UInt32), typeof(TestEnum), true, (UInt32)enumValue, enumValue), - (typeof(UInt32), typeof(UInt16), true, uint32Value, (UInt16)uint32Value), - (typeof(UInt32), typeof(UInt32), true, uint32Value, uint32Value), - (typeof(UInt32), typeof(UInt64), true, uint32Value, (UInt64)uint32Value), - (typeof(UInt64), typeof(Boolean), true, (UInt64)1, true), - (typeof(UInt64), typeof(Byte), true, uint64Value, (Byte) uint64Value), - (typeof(UInt64), typeof(Char), true, uint64Value, (Char) uint64Value), - (typeof(UInt64), typeof(Decimal), true, uint64Value, (Decimal) uint64Value), - (typeof(UInt64), typeof(Double), true, uint64Value, (Double)uint64Value), - (typeof(UInt64), typeof(Int16), true, uint64Value, (Int16)uint64Value), - (typeof(UInt64), typeof(Int32), true, uint64Value, (Int32)uint64Value), - (typeof(UInt64), typeof(Int64), true, uint64Value, (Int64)uint64Value), - (typeof(UInt64), typeof(Object), true, uint64Value, uint64Value), - (typeof(UInt64), typeof(SByte), true, uint64Value, (SByte)uint64Value), - (typeof(UInt64), typeof(Single), true, uint64Value, (Single)uint64Value), - (typeof(UInt64), typeof(String), true, uint64Value, uint64Value.ToString(CultureInfo.InvariantCulture)), - (typeof(UInt64), typeof(TestEnum), true, (UInt64)enumValue, enumValue), - (typeof(UInt64), typeof(UInt16), true, uint64Value, (UInt16)uint64Value), - (typeof(UInt64), typeof(UInt32), true, uint64Value, (UInt32)uint64Value), - (typeof(UInt64), typeof(UInt64), true, uint64Value, uint64Value), - (typeof(UIntPtr), typeof(Object), true, uintPtrValue, uintPtrValue), - (typeof(UIntPtr), typeof(UIntPtr), true, uintPtrValue, uintPtrValue), - (typeof(Char), typeof(Guid), false, charValue, null), - (typeof(Int32), typeof(Guid), false, int32Value, null), - (typeof(DateTime), typeof(Guid), false, dateTimeValue, null), - (typeof(Guid), typeof(DateTime), false, guidValue, null), - (typeof(DateOnly), typeof(DateTime), false, dateOnlyValue, null), - (typeof(TimeOnly), typeof(TimeSpan), false, timeOnlyValue, null), - (typeof(DateOnly), typeof(Guid), false, dateOnlyValue, null), - (typeof(TimeOnly), typeof(Guid), false, timeOnlyValue, null) - }; - - // @formatter:on - } } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/EntityManipulatorTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/EntityManipulatorTests.cs index b644cff..1a6b7f2 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/EntityManipulatorTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/EntityManipulatorTests.cs @@ -9,6 +9,15 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters; public class EntityManipulatorTests : UnitTestsBase { + public static IEnumerable> GetManipulators() + { + yield return new(new MySqlEntityManipulator(new())); + yield return new(new OracleEntityManipulator(new())); + yield return new(new PostgreSqlEntityManipulator(new())); + yield return new(new SqliteEntityManipulator(new())); + yield return new(new SqlServerEntityManipulator(new())); + } + [Theory] [MemberData(nameof(GetManipulators))] public void ShouldGuardAgainstNullArguments(IEntityManipulator manipulator) @@ -64,13 +73,4 @@ public void ShouldGuardAgainstNullArguments(IEntityManipulator manipulator) manipulator.UpdateEntityAsync(this.MockDbConnection, entity, null, CancellationToken.None) ); } - - public static IEnumerable> GetManipulators() - { - yield return new(new MySqlEntityManipulator(new())); - yield return new(new OracleEntityManipulator(new())); - yield return new(new PostgreSqlEntityManipulator(new())); - yield return new(new SqliteEntityManipulator(new())); - yield return new(new SqlServerEntityManipulator(new())); - } } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlConfigurationExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlConfigurationExtensionsTests.cs index 0f56ab9..128d024 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlConfigurationExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlConfigurationExtensionsTests.cs @@ -1,7 +1,6 @@ using MySqlConnector; using RentADeveloper.DbConnectionPlus.DatabaseAdapters.MySql; - namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.MySql; public class MySqlConfigurationExtensionsTests : UnitTestsBase @@ -23,4 +22,4 @@ public void UseMySql_ShouldRegisterMySqlAdapter() adapter.Should().NotBeNull(); adapter.Should().BeOfType(); } -} \ No newline at end of file +} diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs index ca7e26b..a31edc3 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlDatabaseAdapterTests.cs @@ -4,20 +4,20 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.MySql; public class MySqlDatabaseAdapterTests : UnitTestsBase { + private readonly MySqlDatabaseAdapter adapter = new(); + [Fact] public void BindParameterValue_BytesValue_ShouldSetDbTypeAndValue() { var parameter = Substitute.For(); - var value = Generate.Single(); + var value = Generate.Single(); this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.Binary); + parameter.DbType.Should().Be(DbType.Binary); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -29,11 +29,9 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.DateTime); + parameter.DbType.Should().Be(DbType.DateTime); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -47,11 +45,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.Int32); + parameter.DbType.Should().Be(DbType.Int32); - parameter.Value - .Should().Be((Int32)enumValue); + parameter.Value.Should().Be((int)enumValue); } [Fact] @@ -65,11 +61,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBi this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.String); + parameter.DbType.Should().Be(DbType.String); - parameter.Value - .Should().Be(enumValue.ToString()); + parameter.Value.Should().Be(enumValue.ToString()); } [Fact] @@ -81,34 +75,30 @@ public void BindParameterValue_ShouldSetValue() this.adapter.BindParameterValue(parameter, value); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] public void EntityManipulator_ShouldReturnManipulator() => - this.adapter.EntityManipulator - .Should().BeOfType(); + this.adapter.EntityManipulator.Should().BeOfType(); [Fact] public void FormatParameterName_ShouldFormatParameterName() => - this.adapter.FormatParameterName("Param1") - .Should().Be("@Param1"); + this.adapter.FormatParameterName("Param1").Should().Be("@Param1"); [Fact] public void GetDataType_EnumType_EnumSerializationModeIsInteger_ShouldReturnInt() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().Be("INT"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers).Should().Be("INT"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers) - .Should().Be("INT"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers).Should().Be("INT"); } [Fact] public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(TestEnum), (EnumSerializationMode)999)) - .Should().Throw() + .Should() + .Throw() .WithMessage( $"The {nameof(EnumSerializationMode)} '999' ({typeof(EnumSerializationMode)}) is not supported.*" ); @@ -116,89 +106,77 @@ public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow [Fact] public void GetDataType_EnumType_EnumSerializationModeIsString_ShouldReturnVarchar() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().Be("VARCHAR(200)"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings).Should().Be("VARCHAR(200)"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings) - .Should().Be("VARCHAR(200)"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings).Should().Be("VARCHAR(200)"); } [Theory] - [InlineData(typeof(Boolean?), "TINYINT(1)")] - [InlineData(typeof(Boolean), "TINYINT(1)")] - [InlineData(typeof(Byte?), "TINYINT UNSIGNED")] - [InlineData(typeof(Byte), "TINYINT UNSIGNED")] - [InlineData(typeof(Byte[]), "BLOB")] - [InlineData(typeof(Char?), "CHAR(1)")] - [InlineData(typeof(Char), "CHAR(1)")] + [InlineData(typeof(bool?), "TINYINT(1)")] + [InlineData(typeof(bool), "TINYINT(1)")] + [InlineData(typeof(byte?), "TINYINT UNSIGNED")] + [InlineData(typeof(byte), "TINYINT UNSIGNED")] + [InlineData(typeof(byte[]), "BLOB")] + [InlineData(typeof(char?), "CHAR(1)")] + [InlineData(typeof(char), "CHAR(1)")] [InlineData(typeof(DateOnly?), "DATE")] [InlineData(typeof(DateOnly), "DATE")] [InlineData(typeof(DateTime?), "DATETIME")] [InlineData(typeof(DateTime), "DATETIME")] - [InlineData(typeof(Decimal?), "DECIMAL(65,30)")] - [InlineData(typeof(Decimal), "DECIMAL(65,30)")] - [InlineData(typeof(Double?), "DOUBLE")] - [InlineData(typeof(Double), "DOUBLE")] + [InlineData(typeof(decimal?), "DECIMAL(65,30)")] + [InlineData(typeof(decimal), "DECIMAL(65,30)")] + [InlineData(typeof(double?), "DOUBLE")] + [InlineData(typeof(double), "DOUBLE")] [InlineData(typeof(Guid?), "CHAR(36)")] [InlineData(typeof(Guid), "CHAR(36)")] - [InlineData(typeof(Int16?), "SMALLINT")] - [InlineData(typeof(Int16), "SMALLINT")] - [InlineData(typeof(Int32?), "INT")] - [InlineData(typeof(Int32), "INT")] - [InlineData(typeof(Int64?), "BIGINT")] - [InlineData(typeof(Int64), "BIGINT")] - [InlineData(typeof(Single?), "FLOAT")] - [InlineData(typeof(Single), "FLOAT")] - [InlineData(typeof(String), "TEXT")] + [InlineData(typeof(short?), "SMALLINT")] + [InlineData(typeof(short), "SMALLINT")] + [InlineData(typeof(int?), "INT")] + [InlineData(typeof(int), "INT")] + [InlineData(typeof(long?), "BIGINT")] + [InlineData(typeof(long), "BIGINT")] + [InlineData(typeof(float?), "FLOAT")] + [InlineData(typeof(float), "FLOAT")] + [InlineData(typeof(string), "TEXT")] [InlineData(typeof(TimeOnly?), "TIME")] [InlineData(typeof(TimeOnly), "TIME")] [InlineData(typeof(TimeSpan?), "TIME")] [InlineData(typeof(TimeSpan), "TIME")] - public void GetDataType_SupportedTypeType_ShouldReturnMySqlDataType(Type type, String expectedResult) => - this.adapter.GetDataType(type, EnumSerializationMode.Strings) - .Should().Be(expectedResult); + public void GetDataType_SupportedTypeType_ShouldReturnMySqlDataType(Type type, string expectedResult) => + this.adapter.GetDataType(type, EnumSerializationMode.Strings).Should().Be(expectedResult); [Fact] public void GetDataType_UnsupportedType_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(Entity), EnumSerializationMode.Strings)) - .Should().Throw() + .Should() + .Throw() .WithMessage($"Could not map the type {typeof(Entity)} to a MySQL data type.*"); [Fact] public void QuoteIdentifier_ShouldQuoteIdentifier() => - this.adapter.QuoteIdentifier("MyTable") - .Should().Be("`MyTable`"); + this.adapter.QuoteIdentifier("MyTable").Should().Be("`MyTable`"); [Fact] public void QuoteTemporaryTableName_ShouldQuoteTableName() => - this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection) - .Should().Be("`TempTable`"); + this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection).Should().Be("`TempTable`"); [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.adapter.BindParameterValue(Substitute.For(), null) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.BindParameterValue(Substitute.For(), null)); ArgumentNullGuardVerifier.Verify(() => this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None) ); - ArgumentNullGuardVerifier.Verify(() => - this.adapter.GetDataType(typeof(Int32), EnumSerializationMode.Integers) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.GetDataType(typeof(int), EnumSerializationMode.Integers)); } [Fact] public void TemporaryTableBuilder_ShouldReturnBuilder() => - this.adapter.TemporaryTableBuilder - .Should().BeOfType(); + this.adapter.TemporaryTableBuilder.Should().BeOfType(); [Fact] public void WasSqlStatementCancelledByCancellationToken_ShouldAlwaysReturnFalse() => - this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None) - .Should().BeFalse(); - - private readonly MySqlDatabaseAdapter adapter = new(); + this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None).Should().BeFalse(); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlTemporaryTableBuilderTests.cs index eb7f7f7..d6d1e97 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/MySql/MySqlTemporaryTableBuilderTests.cs @@ -4,49 +4,47 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.MySql; public class MySqlTemporaryTableBuilderTests : UnitTestsBase { + private readonly MySqlTemporaryTableBuilder builder = new(new()); + [Fact] - public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); } [Fact] - public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(int))) + .Should() + .Throw(); - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int))) + .Should() + .Throw(); } [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - new MySqlTemporaryTableBuilder(new()) - ); + ArgumentNullGuardVerifier.Verify(() => new MySqlTemporaryTableBuilder(new())); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); } - - private readonly MySqlTemporaryTableBuilder builder = new(new()); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleConfigurationExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleConfigurationExtensionsTests.cs index bf3714f..b153733 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleConfigurationExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleConfigurationExtensionsTests.cs @@ -22,4 +22,4 @@ public void UseOracle_ShouldRegisterOracleAdapter() adapter.Should().NotBeNull(); adapter.Should().BeOfType(); } -} \ No newline at end of file +} diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs index 91bde5b..4aee9ee 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleDatabaseAdapterTests.cs @@ -5,25 +5,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.Oracle; public class OracleDatabaseAdapterTests : UnitTestsBase { + private readonly OracleDatabaseAdapter adapter = new(); + [Fact] public void AllowTemporaryTables_ShouldReturnFalsePerDefault() => - OracleDatabaseAdapter.AllowTemporaryTables - .Should().BeFalse(); + OracleDatabaseAdapter.AllowTemporaryTables.Should().BeFalse(); [Fact] public void BindParameterValue_BytesValue_ShouldSetDbTypeAndValue() { var parameter = Substitute.For(); - var value = Generate.Single(); + var value = Generate.Single(); this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.Binary); + parameter.DbType.Should().Be(DbType.Binary); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -35,11 +34,9 @@ public void BindParameterValue_DateOnlyValue_ShouldSetDbTypeAndValue() this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.Date); + parameter.DbType.Should().Be(DbType.Date); - parameter.Value - .Should().Be(value.ToDateTime(TimeOnly.MinValue)); + parameter.Value.Should().Be(value.ToDateTime(TimeOnly.MinValue)); } [Fact] @@ -51,11 +48,9 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.DateTime); + parameter.DbType.Should().Be(DbType.DateTime); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -69,11 +64,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.Int32); + parameter.DbType.Should().Be(DbType.Int32); - parameter.Value - .Should().Be((Int32)enumValue); + parameter.Value.Should().Be((int)enumValue); } [Fact] @@ -87,11 +80,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBi this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.String); + parameter.DbType.Should().Be(DbType.String); - parameter.Value - .Should().Be(enumValue.ToString()); + parameter.Value.Should().Be(enumValue.ToString()); } [Fact] @@ -103,11 +94,9 @@ public void BindParameterValue_GuidValue_ShouldSetDbTypeAndValue() this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.Binary); + parameter.DbType.Should().Be(DbType.Binary); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -119,8 +108,7 @@ public void BindParameterValue_ShouldSetValue() this.adapter.BindParameterValue(parameter, value); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -132,40 +120,34 @@ public void BindParameterValue_TimeOnlyValue_ShouldSetDbTypeAndValue() this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.Time); + parameter.DbType.Should().Be(DbType.Time); - (parameter as OracleParameter)?.OracleDbType - .Should().Be(OracleDbType.IntervalDS); + (parameter as OracleParameter)?.OracleDbType.Should().Be(OracleDbType.IntervalDS); - parameter.Value - .Should().Be(value.ToTimeSpan()); + parameter.Value.Should().Be(value.ToTimeSpan()); } [Fact] public void EntityManipulator_ShouldReturnManipulator() => - this.adapter.EntityManipulator - .Should().BeOfType(); + this.adapter.EntityManipulator.Should().BeOfType(); [Fact] public void FormatParameterName_ShouldFormatParameterName() => - this.adapter.FormatParameterName("Param1") - .Should().Be(":\"Param1\""); + this.adapter.FormatParameterName("Param1").Should().Be(":\"Param1\""); [Fact] public void GetDataType_EnumType_EnumSerializationModeIsInteger_ShouldReturnNumber() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().Be("NUMBER(10)"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers).Should().Be("NUMBER(10)"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers) - .Should().Be("NUMBER(10)"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers).Should().Be("NUMBER(10)"); } [Fact] public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(TestEnum), (EnumSerializationMode)999)) - .Should().Throw() + .Should() + .Throw() .WithMessage( $"The {nameof(EnumSerializationMode)} '999' ({typeof(EnumSerializationMode)}) is not supported.*" ); @@ -173,70 +155,67 @@ public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow [Fact] public void GetDataType_EnumType_EnumSerializationModeIsString_ShouldReturnNVarchar2() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().Be("NVARCHAR2(200)"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings).Should().Be("NVARCHAR2(200)"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings) - .Should().Be("NVARCHAR2(200)"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings).Should().Be("NVARCHAR2(200)"); } [Theory] - [InlineData(typeof(Boolean?), "NUMBER(1)")] - [InlineData(typeof(Boolean), "NUMBER(1)")] - [InlineData(typeof(Byte), "NUMBER(3)")] - [InlineData(typeof(Byte?), "NUMBER(3)")] - [InlineData(typeof(Byte[]), "RAW(2000)")] - [InlineData(typeof(Char?), "CHAR(1)")] - [InlineData(typeof(Char), "CHAR(1)")] + [InlineData(typeof(bool?), "NUMBER(1)")] + [InlineData(typeof(bool), "NUMBER(1)")] + [InlineData(typeof(byte), "NUMBER(3)")] + [InlineData(typeof(byte?), "NUMBER(3)")] + [InlineData(typeof(byte[]), "RAW(2000)")] + [InlineData(typeof(char?), "CHAR(1)")] + [InlineData(typeof(char), "CHAR(1)")] [InlineData(typeof(DateOnly?), "DATE")] [InlineData(typeof(DateOnly), "DATE")] [InlineData(typeof(DateTimeOffset?), "TIMESTAMP WITH TIME ZONE")] [InlineData(typeof(DateTimeOffset), "TIMESTAMP WITH TIME ZONE")] [InlineData(typeof(DateTime?), "TIMESTAMP")] [InlineData(typeof(DateTime), "TIMESTAMP")] - [InlineData(typeof(Decimal?), "NUMBER(28,10)")] - [InlineData(typeof(Decimal), "NUMBER(28,10)")] - [InlineData(typeof(Double?), "BINARY_DOUBLE")] - [InlineData(typeof(Double), "BINARY_DOUBLE")] + [InlineData(typeof(decimal?), "NUMBER(28,10)")] + [InlineData(typeof(decimal), "NUMBER(28,10)")] + [InlineData(typeof(double?), "BINARY_DOUBLE")] + [InlineData(typeof(double), "BINARY_DOUBLE")] [InlineData(typeof(Guid?), "RAW(16)")] [InlineData(typeof(Guid), "RAW(16)")] - [InlineData(typeof(Int16?), "NUMBER(5)")] - [InlineData(typeof(Int16), "NUMBER(5)")] - [InlineData(typeof(Int32?), "NUMBER(10)")] - [InlineData(typeof(Int32), "NUMBER(10)")] - [InlineData(typeof(Int64?), "NUMBER(19)")] - [InlineData(typeof(Int64), "NUMBER(19)")] - [InlineData(typeof(Single?), "BINARY_FLOAT")] - [InlineData(typeof(Single), "BINARY_FLOAT")] - [InlineData(typeof(String), "NVARCHAR2(2000)")] + [InlineData(typeof(short?), "NUMBER(5)")] + [InlineData(typeof(short), "NUMBER(5)")] + [InlineData(typeof(int?), "NUMBER(10)")] + [InlineData(typeof(int), "NUMBER(10)")] + [InlineData(typeof(long?), "NUMBER(19)")] + [InlineData(typeof(long), "NUMBER(19)")] + [InlineData(typeof(float?), "BINARY_FLOAT")] + [InlineData(typeof(float), "BINARY_FLOAT")] + [InlineData(typeof(string), "NVARCHAR2(2000)")] [InlineData(typeof(TimeOnly?), "INTERVAL DAY TO SECOND")] [InlineData(typeof(TimeOnly), "INTERVAL DAY TO SECOND")] [InlineData(typeof(TimeSpan?), "INTERVAL DAY TO SECOND")] [InlineData(typeof(TimeSpan), "INTERVAL DAY TO SECOND")] - public void GetDataType_SupportedTypeType_ShouldReturnSqlServerDataType(Type type, String expectedResult) => - this.adapter.GetDataType(type, EnumSerializationMode.Strings) - .Should().Be(expectedResult); + public void GetDataType_SupportedTypeType_ShouldReturnSqlServerDataType(Type type, string expectedResult) => + this.adapter.GetDataType(type, EnumSerializationMode.Strings).Should().Be(expectedResult); [Fact] public void GetDataType_UnsupportedType_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(Entity), EnumSerializationMode.Strings)) - .Should().Throw() + .Should() + .Throw() .WithMessage($"Could not map the type {typeof(Entity)} to an Oracle data type.*"); [Fact] public void GetDbType_EnumType_EnumSerializationModeIsInteger_ShouldReturnInt32() { - this.adapter.GetDbType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().Be(DbType.Int32); + this.adapter.GetDbType(typeof(TestEnum), EnumSerializationMode.Integers).Should().Be(DbType.Int32); - this.adapter.GetDbType(typeof(TestEnum?), EnumSerializationMode.Integers) - .Should().Be(DbType.Int32); + this.adapter.GetDbType(typeof(TestEnum?), EnumSerializationMode.Integers).Should().Be(DbType.Int32); } [Fact] public void GetDbType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow() => Invoking(() => this.adapter.GetDbType(typeof(TestEnum), (EnumSerializationMode)999)) - .Should().Throw() + .Should() + .Throw() .WithMessage( $"The {nameof(EnumSerializationMode)} '999' ({typeof(EnumSerializationMode)}) is not supported.*" ); @@ -244,80 +223,72 @@ public void GetDbType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow() [Fact] public void GetDbType_EnumType_EnumSerializationModeIsString_ShouldReturnString() { - this.adapter.GetDbType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().Be(DbType.String); + this.adapter.GetDbType(typeof(TestEnum), EnumSerializationMode.Strings).Should().Be(DbType.String); - this.adapter.GetDbType(typeof(TestEnum?), EnumSerializationMode.Strings) - .Should().Be(DbType.String); + this.adapter.GetDbType(typeof(TestEnum?), EnumSerializationMode.Strings).Should().Be(DbType.String); } [Theory] - [InlineData(typeof(Boolean?), DbType.Boolean)] - [InlineData(typeof(Boolean), DbType.Boolean)] - [InlineData(typeof(Byte), DbType.Byte)] - [InlineData(typeof(Byte?), DbType.Byte)] - [InlineData(typeof(Byte[]), DbType.Binary)] - [InlineData(typeof(Char?), DbType.StringFixedLength)] - [InlineData(typeof(Char), DbType.StringFixedLength)] + [InlineData(typeof(bool?), DbType.Boolean)] + [InlineData(typeof(bool), DbType.Boolean)] + [InlineData(typeof(byte), DbType.Byte)] + [InlineData(typeof(byte?), DbType.Byte)] + [InlineData(typeof(byte[]), DbType.Binary)] + [InlineData(typeof(char?), DbType.StringFixedLength)] + [InlineData(typeof(char), DbType.StringFixedLength)] [InlineData(typeof(DateOnly?), DbType.Date)] [InlineData(typeof(DateOnly), DbType.Date)] [InlineData(typeof(DateTimeOffset?), DbType.DateTimeOffset)] [InlineData(typeof(DateTimeOffset), DbType.DateTimeOffset)] [InlineData(typeof(DateTime?), DbType.DateTime)] [InlineData(typeof(DateTime), DbType.DateTime)] - [InlineData(typeof(Decimal?), DbType.Decimal)] - [InlineData(typeof(Decimal), DbType.Decimal)] - [InlineData(typeof(Double?), DbType.Double)] - [InlineData(typeof(Double), DbType.Double)] + [InlineData(typeof(decimal?), DbType.Decimal)] + [InlineData(typeof(decimal), DbType.Decimal)] + [InlineData(typeof(double?), DbType.Double)] + [InlineData(typeof(double), DbType.Double)] [InlineData(typeof(Guid?), DbType.Guid)] [InlineData(typeof(Guid), DbType.Guid)] - [InlineData(typeof(Int16?), DbType.Int16)] - [InlineData(typeof(Int16), DbType.Int16)] - [InlineData(typeof(Int32?), DbType.Int32)] - [InlineData(typeof(Int32), DbType.Int32)] - [InlineData(typeof(Int64?), DbType.Int64)] - [InlineData(typeof(Int64), DbType.Int64)] - [InlineData(typeof(Single?), DbType.Single)] - [InlineData(typeof(Single), DbType.Single)] - [InlineData(typeof(String), DbType.String)] + [InlineData(typeof(short?), DbType.Int16)] + [InlineData(typeof(short), DbType.Int16)] + [InlineData(typeof(int?), DbType.Int32)] + [InlineData(typeof(int), DbType.Int32)] + [InlineData(typeof(long?), DbType.Int64)] + [InlineData(typeof(long), DbType.Int64)] + [InlineData(typeof(float?), DbType.Single)] + [InlineData(typeof(float), DbType.Single)] + [InlineData(typeof(string), DbType.String)] [InlineData(typeof(TimeOnly?), DbType.Time)] [InlineData(typeof(TimeOnly), DbType.Time)] [InlineData(typeof(TimeSpan?), DbType.Time)] [InlineData(typeof(TimeSpan), DbType.Time)] public void GetDbType_SupportedTypeType_ShouldReturnSqlServerDataType(Type type, DbType expectedResult) => - this.adapter.GetDbType(type, EnumSerializationMode.Strings) - .Should().Be(expectedResult); + this.adapter.GetDbType(type, EnumSerializationMode.Strings).Should().Be(expectedResult); [Fact] public void GetDbType_UnsupportedType_ShouldThrow() => Invoking(() => this.adapter.GetDbType(typeof(Entity), EnumSerializationMode.Strings)) - .Should().Throw() + .Should() + .Throw() .WithMessage($"Could not map the type {typeof(Entity)} to a {typeof(DbType)} value.*"); [Fact] public void QuoteIdentifier_ShouldQuoteIdentifier() => - this.adapter.QuoteIdentifier("MyTable") - .Should().Be("\"MyTable\""); + this.adapter.QuoteIdentifier("MyTable").Should().Be("\"MyTable\""); [Fact] public void QuoteTemporaryTableName_ShouldQuoteTableName() { this.MockDbCommand.ExecuteScalar().Returns("MockPrefix"); - this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection) - .Should().Be("\"MockPrefixTempTable\""); + this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection).Should().Be("\"MockPrefixTempTable\""); } [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.adapter.BindParameterValue(Substitute.For(), null) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.BindParameterValue(Substitute.For(), null)); - ArgumentNullGuardVerifier.Verify(() => - this.adapter.SupportsTemporaryTables(this.MockDbConnection) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.SupportsTemporaryTables(this.MockDbConnection)); ArgumentNullGuardVerifier.Verify(() => this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None) @@ -331,8 +302,7 @@ public void SupportsTemporaryTables_OracleVersionEqualToOrGreaterThan18_ShouldRe // Oracle version is 18 or higher. this.MockDbDataReader.Read().Returns(true); - this.adapter.SupportsTemporaryTables(this.MockDbConnection) - .Should().BeTrue(); + this.adapter.SupportsTemporaryTables(this.MockDbConnection).Should().BeTrue(); } [Fact] @@ -342,8 +312,7 @@ public void SupportsTemporaryTables_OracleVersionLessThan18_ShouldReturnFalse() // the Oracle version is lower than 18. this.MockDbDataReader.Read().Returns(false); - this.adapter.SupportsTemporaryTables(this.MockDbConnection) - .Should().BeFalse(); + this.adapter.SupportsTemporaryTables(this.MockDbConnection).Should().BeFalse(); } [Fact] @@ -352,12 +321,13 @@ public void TemporaryTableBuilder_AllowTemporaryTablesIsFalse_ShouldThrow() OracleDatabaseAdapter.AllowTemporaryTables = false; Invoking(() => this.adapter.TemporaryTableBuilder) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The temporary tables feature of DbConnectionPlus is currently disabled for Oracle databases. " + - $"To enable it set {typeof(OracleDatabaseAdapter)}.AllowTemporaryTables to true, but be sure to " + - "read the documentation first, because enabling this feature has implications for transaction " + - "management." + "The temporary tables feature of DbConnectionPlus is currently disabled for Oracle databases. " + + $"To enable it set {typeof(OracleDatabaseAdapter)}.AllowTemporaryTables to true, but be sure to " + + "read the documentation first, because enabling this feature has implications for transaction " + + "management." ); } @@ -366,9 +336,6 @@ public void TemporaryTableBuilder_AllowTemporaryTablesIsTrue_ShouldReturnBuilder { OracleDatabaseAdapter.AllowTemporaryTables = true; - this.adapter.TemporaryTableBuilder - .Should().BeOfType(); + this.adapter.TemporaryTableBuilder.Should().BeOfType(); } - - private readonly OracleDatabaseAdapter adapter = new(); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleTemporaryTableBuilderTests.cs index c478281..c53c6de 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Oracle/OracleTemporaryTableBuilderTests.cs @@ -4,87 +4,81 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.Oracle; public class OracleTemporaryTableBuilderTests : UnitTestsBase { + private readonly OracleTemporaryTableBuilder builder = new(new()); + [Fact] - public void BuildTemporaryTable_AllowTemporaryTablesIsFalse_ShouldThrow() + public Task BuildTemporaryTableAsync_AllowTemporaryTablesIsFalse_ShouldThrow() { OracleDatabaseAdapter.AllowTemporaryTables = false; - Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + return Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ) - .Should().Throw() + .Should() + .ThrowAsync() .WithMessage( - "The temporary tables feature of DbConnectionPlus is currently disabled for Oracle databases. " + - $"To enable it set {typeof(OracleDatabaseAdapter)}.AllowTemporaryTables to true, but be sure to " + - "read the documentation first, because enabling this feature has implications for transaction " + - "management." + "The temporary tables feature of DbConnectionPlus is currently disabled for Oracle databases. " + + $"To enable it set {typeof(OracleDatabaseAdapter)}.AllowTemporaryTables to true, but be sure to " + + "read the documentation first, because enabling this feature has implications for transaction " + + "management." ); } [Fact] - public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); } [Fact] - public Task BuildTemporaryTableAsync_AllowTemporaryTablesIsFalse_ShouldThrow() + public void BuildTemporaryTable_AllowTemporaryTablesIsFalse_ShouldThrow() { OracleDatabaseAdapter.AllowTemporaryTables = false; - return Invoking(() => this.builder.BuildTemporaryTableAsync( - this.MockDbConnection, - null, - "Name", - new[] { 1 }, - typeof(Int32) - ) - ) - .Should().ThrowAsync() + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int))) + .Should() + .Throw() .WithMessage( - "The temporary tables feature of DbConnectionPlus is currently disabled for Oracle databases. " + - $"To enable it set {typeof(OracleDatabaseAdapter)}.AllowTemporaryTables to true, but be sure to " + - "read the documentation first, because enabling this feature has implications for transaction " + - "management." + "The temporary tables feature of DbConnectionPlus is currently disabled for Oracle databases. " + + $"To enable it set {typeof(OracleDatabaseAdapter)}.AllowTemporaryTables to true, but be sure to " + + "read the documentation first, because enabling this feature has implications for transaction " + + "management." ); } [Fact] - public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(int))) + .Should() + .Throw(); - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int))) + .Should() + .Throw(); } [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - new OracleTemporaryTableBuilder(new()) - ); + ArgumentNullGuardVerifier.Verify(() => new OracleTemporaryTableBuilder(new())); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); } - - private readonly OracleTemporaryTableBuilder builder = new(new()); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlConfigurationExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlConfigurationExtensionsTests.cs index b1245ab..b7ca612 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlConfigurationExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlConfigurationExtensionsTests.cs @@ -22,4 +22,4 @@ public void UsePostgreSql_ShouldRegisterPostgreSqlAdapter() adapter.Should().NotBeNull(); adapter.Should().BeOfType(); } -} \ No newline at end of file +} diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs index 74e0776..44def57 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlDatabaseAdapterTests.cs @@ -5,20 +5,20 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.PostgreSql; public class PostgreSqlDatabaseAdapterTests : UnitTestsBase { + private readonly PostgreSqlDatabaseAdapter adapter = new(); + [Fact] public void BindParameterValue_BytesValue_ShouldSetDbTypeAndValue() { var parameter = Substitute.For(); - var value = Generate.Single(); + var value = Generate.Single(); this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.Binary); + parameter.DbType.Should().Be(DbType.Binary); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -30,11 +30,9 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.DateTime2); + parameter.DbType.Should().Be(DbType.DateTime2); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -48,11 +46,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.Int32); + parameter.DbType.Should().Be(DbType.Int32); - parameter.Value - .Should().Be((Int32)enumValue); + parameter.Value.Should().Be((int)enumValue); } [Fact] @@ -66,11 +62,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBi this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.String); + parameter.DbType.Should().Be(DbType.String); - parameter.Value - .Should().Be(enumValue.ToString()); + parameter.Value.Should().Be(enumValue.ToString()); } [Fact] @@ -82,34 +76,30 @@ public void BindParameterValue_ShouldSetValue() this.adapter.BindParameterValue(parameter, value); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] public void EntityManipulator_ShouldReturnManipulator() => - this.adapter.EntityManipulator - .Should().BeOfType(); + this.adapter.EntityManipulator.Should().BeOfType(); [Fact] public void FormatParameterName_ShouldFormatParameterName() => - this.adapter.FormatParameterName("Param1") - .Should().Be("@Param1"); + this.adapter.FormatParameterName("Param1").Should().Be("@Param1"); [Fact] public void GetDataType_EnumType_EnumSerializationModeIsInteger_ShouldReturnInteger() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().Be("integer"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers).Should().Be("integer"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers) - .Should().Be("integer"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers).Should().Be("integer"); } [Fact] public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(TestEnum), (EnumSerializationMode)999)) - .Should().Throw() + .Should() + .Throw() .WithMessage( $"The {nameof(EnumSerializationMode)} '999' ({typeof(EnumSerializationMode)}) is not supported.*" ); @@ -117,129 +107,118 @@ public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow [Fact] public void GetDataType_EnumType_EnumSerializationModeIsString_ShouldReturnCharacterVarying() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().Be("character varying(200)"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings).Should().Be("character varying(200)"); this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings) - .Should().Be("character varying(200)"); + .Should() + .Be("character varying(200)"); } [Theory] - [InlineData(typeof(Boolean?), "boolean")] - [InlineData(typeof(Boolean), "boolean")] - [InlineData(typeof(Byte?), "smallint")] - [InlineData(typeof(Byte), "smallint")] - [InlineData(typeof(Byte[]), "bytea")] - [InlineData(typeof(Char?), "char(1)")] - [InlineData(typeof(Char), "char(1)")] + [InlineData(typeof(bool?), "boolean")] + [InlineData(typeof(bool), "boolean")] + [InlineData(typeof(byte?), "smallint")] + [InlineData(typeof(byte), "smallint")] + [InlineData(typeof(byte[]), "bytea")] + [InlineData(typeof(char?), "char(1)")] + [InlineData(typeof(char), "char(1)")] [InlineData(typeof(DateOnly?), "date")] [InlineData(typeof(DateOnly), "date")] [InlineData(typeof(DateTime?), "timestamp without time zone")] [InlineData(typeof(DateTime), "timestamp without time zone")] - [InlineData(typeof(Decimal?), "decimal")] - [InlineData(typeof(Decimal), "decimal")] - [InlineData(typeof(Double?), "double precision")] - [InlineData(typeof(Double), "double precision")] + [InlineData(typeof(decimal?), "decimal")] + [InlineData(typeof(decimal), "decimal")] + [InlineData(typeof(double?), "double precision")] + [InlineData(typeof(double), "double precision")] [InlineData(typeof(Guid?), "uuid")] [InlineData(typeof(Guid), "uuid")] - [InlineData(typeof(Int16?), "smallint")] - [InlineData(typeof(Int16), "smallint")] - [InlineData(typeof(Int32?), "integer")] - [InlineData(typeof(Int32), "integer")] - [InlineData(typeof(Int64?), "bigint")] - [InlineData(typeof(Int64), "bigint")] - [InlineData(typeof(Single?), "real")] - [InlineData(typeof(Single), "real")] - [InlineData(typeof(String), "text")] + [InlineData(typeof(short?), "smallint")] + [InlineData(typeof(short), "smallint")] + [InlineData(typeof(int?), "integer")] + [InlineData(typeof(int), "integer")] + [InlineData(typeof(long?), "bigint")] + [InlineData(typeof(long), "bigint")] + [InlineData(typeof(float?), "real")] + [InlineData(typeof(float), "real")] + [InlineData(typeof(string), "text")] [InlineData(typeof(TimeOnly?), "time")] [InlineData(typeof(TimeOnly), "time")] [InlineData(typeof(TimeSpan?), "interval")] [InlineData(typeof(TimeSpan), "interval")] - public void GetDataType_SupportedTypeType_ShouldReturnPostgreSqlDataType(Type type, String expectedResult) => - this.adapter.GetDataType(type, EnumSerializationMode.Strings) - .Should().Be(expectedResult); + public void GetDataType_SupportedTypeType_ShouldReturnPostgreSqlDataType(Type type, string expectedResult) => + this.adapter.GetDataType(type, EnumSerializationMode.Strings).Should().Be(expectedResult); [Fact] public void GetDataType_UnsupportedType_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(Entity), EnumSerializationMode.Strings)) - .Should().Throw() + .Should() + .Throw() .WithMessage($"Could not map the type {typeof(Entity)} to a PostgreSQL data type.*"); [Theory] - [InlineData(typeof(Boolean?), NpgsqlDbType.Boolean)] - [InlineData(typeof(Boolean), NpgsqlDbType.Boolean)] - [InlineData(typeof(Byte?), NpgsqlDbType.Smallint)] - [InlineData(typeof(Byte), NpgsqlDbType.Smallint)] - [InlineData(typeof(Byte[]), NpgsqlDbType.Bytea)] - [InlineData(typeof(Char?), NpgsqlDbType.Char)] - [InlineData(typeof(Char), NpgsqlDbType.Char)] + [InlineData(typeof(bool?), NpgsqlDbType.Boolean)] + [InlineData(typeof(bool), NpgsqlDbType.Boolean)] + [InlineData(typeof(byte?), NpgsqlDbType.Smallint)] + [InlineData(typeof(byte), NpgsqlDbType.Smallint)] + [InlineData(typeof(byte[]), NpgsqlDbType.Bytea)] + [InlineData(typeof(char?), NpgsqlDbType.Char)] + [InlineData(typeof(char), NpgsqlDbType.Char)] [InlineData(typeof(DateOnly?), NpgsqlDbType.Date)] [InlineData(typeof(DateOnly), NpgsqlDbType.Date)] [InlineData(typeof(DateTime?), NpgsqlDbType.Timestamp)] [InlineData(typeof(DateTime), NpgsqlDbType.Timestamp)] - [InlineData(typeof(Decimal?), NpgsqlDbType.Numeric)] - [InlineData(typeof(Decimal), NpgsqlDbType.Numeric)] - [InlineData(typeof(Double?), NpgsqlDbType.Double)] - [InlineData(typeof(Double), NpgsqlDbType.Double)] + [InlineData(typeof(decimal?), NpgsqlDbType.Numeric)] + [InlineData(typeof(decimal), NpgsqlDbType.Numeric)] + [InlineData(typeof(double?), NpgsqlDbType.Double)] + [InlineData(typeof(double), NpgsqlDbType.Double)] [InlineData(typeof(Guid?), NpgsqlDbType.Uuid)] [InlineData(typeof(Guid), NpgsqlDbType.Uuid)] - [InlineData(typeof(Int16?), NpgsqlDbType.Smallint)] - [InlineData(typeof(Int16), NpgsqlDbType.Smallint)] - [InlineData(typeof(Int32?), NpgsqlDbType.Integer)] - [InlineData(typeof(Int32), NpgsqlDbType.Integer)] - [InlineData(typeof(Int64?), NpgsqlDbType.Bigint)] - [InlineData(typeof(Int64), NpgsqlDbType.Bigint)] - [InlineData(typeof(Single?), NpgsqlDbType.Real)] - [InlineData(typeof(Single), NpgsqlDbType.Real)] - [InlineData(typeof(String), NpgsqlDbType.Text)] + [InlineData(typeof(short?), NpgsqlDbType.Smallint)] + [InlineData(typeof(short), NpgsqlDbType.Smallint)] + [InlineData(typeof(int?), NpgsqlDbType.Integer)] + [InlineData(typeof(int), NpgsqlDbType.Integer)] + [InlineData(typeof(long?), NpgsqlDbType.Bigint)] + [InlineData(typeof(long), NpgsqlDbType.Bigint)] + [InlineData(typeof(float?), NpgsqlDbType.Real)] + [InlineData(typeof(float), NpgsqlDbType.Real)] + [InlineData(typeof(string), NpgsqlDbType.Text)] [InlineData(typeof(TimeOnly?), NpgsqlDbType.Time)] [InlineData(typeof(TimeOnly), NpgsqlDbType.Time)] [InlineData(typeof(TimeSpan?), NpgsqlDbType.Interval)] [InlineData(typeof(TimeSpan), NpgsqlDbType.Interval)] public void GetDbType_SupportedTypeType_ShouldReturnDbDataType(Type type, NpgsqlDbType expectedResult) => - this.adapter.GetDbType(type, EnumSerializationMode.Strings) - .Should().Be(expectedResult); + this.adapter.GetDbType(type, EnumSerializationMode.Strings).Should().Be(expectedResult); [Fact] public void GetDbType_UnsupportedType_ShouldThrow() => Invoking(() => this.adapter.GetDbType(typeof(Entity), EnumSerializationMode.Strings)) - .Should().Throw() + .Should() + .Throw() .WithMessage($"Could not map the type {typeof(Entity)} to a {typeof(NpgsqlDbType)} value.*"); [Fact] public void QuoteIdentifier_ShouldQuoteIdentifier() => - this.adapter.QuoteIdentifier("MyTable") - .Should().Be("\"MyTable\""); + this.adapter.QuoteIdentifier("MyTable").Should().Be("\"MyTable\""); [Fact] public void QuoteTemporaryTableName_ShouldQuoteTableName() => - this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection) - .Should().Be("\"TempTable\""); + this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection).Should().Be("\"TempTable\""); [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.adapter.BindParameterValue(Substitute.For(), null) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.BindParameterValue(Substitute.For(), null)); ArgumentNullGuardVerifier.Verify(() => this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None) ); - ArgumentNullGuardVerifier.Verify(() => - this.adapter.GetDataType(typeof(Int32), EnumSerializationMode.Integers) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.GetDataType(typeof(int), EnumSerializationMode.Integers)); - ArgumentNullGuardVerifier.Verify(() => - this.adapter.GetDbType(typeof(Int32), EnumSerializationMode.Integers) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.GetDbType(typeof(int), EnumSerializationMode.Integers)); } [Fact] public void TemporaryTableBuilder_ShouldReturnBuilder() => - this.adapter.TemporaryTableBuilder - .Should().BeOfType(); - - private readonly PostgreSqlDatabaseAdapter adapter = new(); + this.adapter.TemporaryTableBuilder.Should().BeOfType(); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilderTests.cs index d6db9a5..30d6e95 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/PostgreSql/PostgreSqlTemporaryTableBuilderTests.cs @@ -4,49 +4,47 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.PostgreSql; public class PostgreSqlTemporaryTableBuilderTests : UnitTestsBase { + private readonly PostgreSqlTemporaryTableBuilder builder = new(new()); + [Fact] - public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); } [Fact] - public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(int))) + .Should() + .Throw(); - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int))) + .Should() + .Throw(); } [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - new PostgreSqlTemporaryTableBuilder(new()) - ); + ArgumentNullGuardVerifier.Verify(() => new PostgreSqlTemporaryTableBuilder(new())); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); } - - private readonly PostgreSqlTemporaryTableBuilder builder = new(new()); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs index 5fd8c0e..3d982dd 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapterTests.cs @@ -4,20 +4,20 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.SqlServer; public class SqlServerDatabaseAdapterTests : UnitTestsBase { + private readonly SqlServerDatabaseAdapter adapter = new(); + [Fact] public void BindParameterValue_BytesValue_ShouldSetDbTypeAndValue() { var parameter = Substitute.For(); - var value = Generate.Single(); + var value = Generate.Single(); this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.Binary); + parameter.DbType.Should().Be(DbType.Binary); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -29,11 +29,9 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.DateTime2); + parameter.DbType.Should().Be(DbType.DateTime2); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -47,11 +45,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.Int32); + parameter.DbType.Should().Be(DbType.Int32); - parameter.Value - .Should().Be((Int32)enumValue); + parameter.Value.Should().Be((int)enumValue); } [Fact] @@ -65,11 +61,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBi this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.String); + parameter.DbType.Should().Be(DbType.String); - parameter.Value - .Should().Be(enumValue.ToString()); + parameter.Value.Should().Be(enumValue.ToString()); } [Fact] @@ -81,34 +75,30 @@ public void BindParameterValue_ShouldSetValue() this.adapter.BindParameterValue(parameter, value); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] public void EntityManipulator_ShouldReturnManipulator() => - this.adapter.EntityManipulator - .Should().BeOfType(); + this.adapter.EntityManipulator.Should().BeOfType(); [Fact] public void FormatParameterName_ShouldFormatParameterName() => - this.adapter.FormatParameterName("Param1") - .Should().Be("@Param1"); + this.adapter.FormatParameterName("Param1").Should().Be("@Param1"); [Fact] public void GetDataType_EnumType_EnumSerializationModeIsInteger_ShouldReturnInt() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().Be("int"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers).Should().Be("int"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers) - .Should().Be("int"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers).Should().Be("int"); } [Fact] public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(TestEnum), (EnumSerializationMode)999)) - .Should().Throw() + .Should() + .Throw() .WithMessage( $"The {nameof(EnumSerializationMode)} '999' ({typeof(EnumSerializationMode)}) is not supported.*" ); @@ -116,87 +106,76 @@ public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow [Fact] public void GetDataType_EnumType_EnumSerializationModeIsString_ShouldReturnNVarchar() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().Be("nvarchar(200)"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings).Should().Be("nvarchar(200)"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings) - .Should().Be("nvarchar(200)"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings).Should().Be("nvarchar(200)"); } [Theory] - [InlineData(typeof(Boolean?), "bit")] - [InlineData(typeof(Boolean), "bit")] - [InlineData(typeof(Byte), "tinyint")] - [InlineData(typeof(Byte?), "tinyint")] - [InlineData(typeof(Byte[]), "varbinary(max)")] - [InlineData(typeof(Char?), "char(1)")] - [InlineData(typeof(Char), "char(1)")] + [InlineData(typeof(bool?), "bit")] + [InlineData(typeof(bool), "bit")] + [InlineData(typeof(byte), "tinyint")] + [InlineData(typeof(byte?), "tinyint")] + [InlineData(typeof(byte[]), "varbinary(max)")] + [InlineData(typeof(char?), "char(1)")] + [InlineData(typeof(char), "char(1)")] [InlineData(typeof(DateOnly?), "date")] [InlineData(typeof(DateOnly), "date")] [InlineData(typeof(DateTimeOffset?), "datetimeoffset")] [InlineData(typeof(DateTimeOffset), "datetimeoffset")] [InlineData(typeof(DateTime?), "datetime2")] [InlineData(typeof(DateTime), "datetime2")] - [InlineData(typeof(Decimal?), "decimal(28,10)")] - [InlineData(typeof(Decimal), "decimal(28,10)")] - [InlineData(typeof(Double?), "float")] - [InlineData(typeof(Double), "float")] + [InlineData(typeof(decimal?), "decimal(28,10)")] + [InlineData(typeof(decimal), "decimal(28,10)")] + [InlineData(typeof(double?), "float")] + [InlineData(typeof(double), "float")] [InlineData(typeof(Guid?), "uniqueidentifier")] [InlineData(typeof(Guid), "uniqueidentifier")] - [InlineData(typeof(Int16?), "smallint")] - [InlineData(typeof(Int16), "smallint")] - [InlineData(typeof(Int32?), "int")] - [InlineData(typeof(Int32), "int")] - [InlineData(typeof(Int64?), "bigint")] - [InlineData(typeof(Int64), "bigint")] - [InlineData(typeof(Object), "sql_variant")] - [InlineData(typeof(Single?), "real")] - [InlineData(typeof(Single), "real")] - [InlineData(typeof(String), "nvarchar(max)")] + [InlineData(typeof(short?), "smallint")] + [InlineData(typeof(short), "smallint")] + [InlineData(typeof(int?), "int")] + [InlineData(typeof(int), "int")] + [InlineData(typeof(long?), "bigint")] + [InlineData(typeof(long), "bigint")] + [InlineData(typeof(object), "sql_variant")] + [InlineData(typeof(float?), "real")] + [InlineData(typeof(float), "real")] + [InlineData(typeof(string), "nvarchar(max)")] [InlineData(typeof(TimeOnly?), "time")] [InlineData(typeof(TimeOnly), "time")] [InlineData(typeof(TimeSpan?), "time")] [InlineData(typeof(TimeSpan), "time")] - public void GetDataType_SupportedTypeType_ShouldReturnSqlServerDataType(Type type, String expectedResult) => - this.adapter.GetDataType(type, EnumSerializationMode.Strings) - .Should().Be(expectedResult); + public void GetDataType_SupportedTypeType_ShouldReturnSqlServerDataType(Type type, string expectedResult) => + this.adapter.GetDataType(type, EnumSerializationMode.Strings).Should().Be(expectedResult); [Fact] public void GetDataType_UnsupportedType_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(Entity), EnumSerializationMode.Strings)) - .Should().Throw() + .Should() + .Throw() .WithMessage($"Could not map the type {typeof(Entity)} to an SQL Server data type.*"); [Fact] public void QuoteIdentifier_ShouldQuoteIdentifier() => - this.adapter.QuoteIdentifier("MyTable") - .Should().Be("[MyTable]"); + this.adapter.QuoteIdentifier("MyTable").Should().Be("[MyTable]"); [Fact] public void QuoteTemporaryTableName_ShouldQuoteTableName() => - this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection) - .Should().Be("[#TempTable]"); + this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection).Should().Be("[#TempTable]"); [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.adapter.BindParameterValue(Substitute.For(), null) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.BindParameterValue(Substitute.For(), null)); ArgumentNullGuardVerifier.Verify(() => this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None) ); - ArgumentNullGuardVerifier.Verify(() => - this.adapter.GetDataType(typeof(Int32), EnumSerializationMode.Integers) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.GetDataType(typeof(int), EnumSerializationMode.Integers)); } [Fact] public void TemporaryTableBuilder_ShouldReturnBuilder() => - this.adapter.TemporaryTableBuilder - .Should().BeOfType(); - - private readonly SqlServerDatabaseAdapter adapter = new(); + this.adapter.TemporaryTableBuilder.Should().BeOfType(); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilderTests.cs index 1f782a2..736e3b7 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqlServerTemporaryTableBuilderTests.cs @@ -4,49 +4,47 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.SqlServer; public class SqlServerTemporaryTableBuilderTests : UnitTestsBase { + private readonly SqlServerTemporaryTableBuilder builder = new(new()); + [Fact] - public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); } [Fact] - public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(int))) + .Should() + .Throw(); - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int))) + .Should() + .Throw(); } [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - new SqlServerTemporaryTableBuilder(new()) - ); + ArgumentNullGuardVerifier.Verify(() => new SqlServerTemporaryTableBuilder(new())); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); } - - private readonly SqlServerTemporaryTableBuilder builder = new(new()); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqliteConfigurationExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqliteConfigurationExtensionsTests.cs index 7f2392a..0767d2f 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqliteConfigurationExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/SqlServer/SqliteConfigurationExtensionsTests.cs @@ -21,4 +21,4 @@ public void UseSqlServer_ShouldRegisterSqlServerAdapter() adapter.Should().NotBeNull(); adapter.Should().BeOfType(); } -} \ No newline at end of file +} diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteConfigurationExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteConfigurationExtensionsTests.cs index 03e18d9..92453a6 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteConfigurationExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteConfigurationExtensionsTests.cs @@ -22,4 +22,4 @@ public void UseSqlite_ShouldRegisterSqliteAdapter() adapter.Should().NotBeNull(); adapter.Should().BeOfType(); } -} \ No newline at end of file +} diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs index 004bdf4..d734903 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteDatabaseAdapterTests.cs @@ -4,20 +4,20 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.Sqlite; public class SqliteDatabaseAdapterTests : UnitTestsBase { + private readonly SqliteDatabaseAdapter adapter = new(); + [Fact] public void BindParameterValue_BytesValue_ShouldSetDbTypeAndValue() { var parameter = Substitute.For(); - var value = Generate.Single(); + var value = Generate.Single(); this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.Binary); + parameter.DbType.Should().Be(DbType.Binary); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -29,11 +29,9 @@ public void BindParameterValue_DateTimeValue_ShouldSetDbTypeAndValue() this.adapter.BindParameterValue(parameter, value); - parameter.DbType - .Should().Be(DbType.DateTime); + parameter.DbType.Should().Be(DbType.DateTime); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] @@ -47,11 +45,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsIntegers_ShouldB this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.Int32); + parameter.DbType.Should().Be(DbType.Int32); - parameter.Value - .Should().Be((Int32)enumValue); + parameter.Value.Should().Be((int)enumValue); } [Fact] @@ -65,11 +61,9 @@ public void BindParameterValue_EnumValue_EnumSerializationModeIsStrings_ShouldBi this.adapter.BindParameterValue(parameter, enumValue); - parameter.DbType - .Should().Be(DbType.String); + parameter.DbType.Should().Be(DbType.String); - parameter.Value - .Should().Be(enumValue.ToString()); + parameter.Value.Should().Be(enumValue.ToString()); } [Fact] @@ -81,34 +75,30 @@ public void BindParameterValue_ShouldSetValue() this.adapter.BindParameterValue(parameter, value); - parameter.Value - .Should().Be(value); + parameter.Value.Should().Be(value); } [Fact] public void EntityManipulator_ShouldReturnManipulator() => - this.adapter.EntityManipulator - .Should().BeOfType(); + this.adapter.EntityManipulator.Should().BeOfType(); [Fact] public void FormatParameterName_ShouldFormatParameterName() => - this.adapter.FormatParameterName("Param1") - .Should().Be("@Param1"); + this.adapter.FormatParameterName("Param1").Should().Be("@Param1"); [Fact] public void GetDataType_EnumType_EnumSerializationModeIsInteger_ShouldReturnInteger() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers) - .Should().Be("INTEGER"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Integers).Should().Be("INTEGER"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers) - .Should().Be("INTEGER"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Integers).Should().Be("INTEGER"); } [Fact] public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(TestEnum), (EnumSerializationMode)999)) - .Should().Throw() + .Should() + .Throw() .WithMessage( $"The {nameof(EnumSerializationMode)} '999' ({typeof(EnumSerializationMode)}) is not supported.*" ); @@ -116,91 +106,79 @@ public void GetDataType_EnumType_EnumSerializationModeIsNotSupported_ShouldThrow [Fact] public void GetDataType_EnumType_EnumSerializationModeIsString_ShouldReturnText() { - this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings) - .Should().Be("TEXT"); + this.adapter.GetDataType(typeof(TestEnum), EnumSerializationMode.Strings).Should().Be("TEXT"); - this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings) - .Should().Be("TEXT"); + this.adapter.GetDataType(typeof(TestEnum?), EnumSerializationMode.Strings).Should().Be("TEXT"); } [Theory] - [InlineData(typeof(Boolean?), "INTEGER")] - [InlineData(typeof(Boolean), "INTEGER")] - [InlineData(typeof(Byte?), "INTEGER")] - [InlineData(typeof(Byte), "INTEGER")] - [InlineData(typeof(Byte[]), "BLOB")] - [InlineData(typeof(Char?), "TEXT")] - [InlineData(typeof(Char), "TEXT")] + [InlineData(typeof(bool?), "INTEGER")] + [InlineData(typeof(bool), "INTEGER")] + [InlineData(typeof(byte?), "INTEGER")] + [InlineData(typeof(byte), "INTEGER")] + [InlineData(typeof(byte[]), "BLOB")] + [InlineData(typeof(char?), "TEXT")] + [InlineData(typeof(char), "TEXT")] [InlineData(typeof(DateOnly?), "TEXT")] [InlineData(typeof(DateOnly), "TEXT")] [InlineData(typeof(DateTime?), "TEXT")] [InlineData(typeof(DateTime), "TEXT")] [InlineData(typeof(DateTimeOffset?), "TEXT")] [InlineData(typeof(DateTimeOffset), "TEXT")] - [InlineData(typeof(Decimal?), "TEXT")] - [InlineData(typeof(Decimal), "TEXT")] - [InlineData(typeof(Double?), "REAL")] - [InlineData(typeof(Double), "REAL")] + [InlineData(typeof(decimal?), "TEXT")] + [InlineData(typeof(decimal), "TEXT")] + [InlineData(typeof(double?), "REAL")] + [InlineData(typeof(double), "REAL")] [InlineData(typeof(Guid?), "TEXT")] [InlineData(typeof(Guid), "TEXT")] - [InlineData(typeof(Int16?), "INTEGER")] - [InlineData(typeof(Int16), "INTEGER")] - [InlineData(typeof(Int32?), "INTEGER")] - [InlineData(typeof(Int32), "INTEGER")] - [InlineData(typeof(Int64?), "INTEGER")] - [InlineData(typeof(Int64), "INTEGER")] - [InlineData(typeof(Single?), "REAL")] - [InlineData(typeof(Single), "REAL")] - [InlineData(typeof(String), "TEXT")] + [InlineData(typeof(short?), "INTEGER")] + [InlineData(typeof(short), "INTEGER")] + [InlineData(typeof(int?), "INTEGER")] + [InlineData(typeof(int), "INTEGER")] + [InlineData(typeof(long?), "INTEGER")] + [InlineData(typeof(long), "INTEGER")] + [InlineData(typeof(float?), "REAL")] + [InlineData(typeof(float), "REAL")] + [InlineData(typeof(string), "TEXT")] [InlineData(typeof(TimeOnly?), "TEXT")] [InlineData(typeof(TimeOnly), "TEXT")] [InlineData(typeof(TimeSpan?), "TEXT")] [InlineData(typeof(TimeSpan), "TEXT")] - public void GetDataType_SupportedTypeType_ShouldReturnSqliteDataType(Type type, String expectedResult) => - this.adapter.GetDataType(type, EnumSerializationMode.Strings) - .Should().Be(expectedResult); + public void GetDataType_SupportedTypeType_ShouldReturnSqliteDataType(Type type, string expectedResult) => + this.adapter.GetDataType(type, EnumSerializationMode.Strings).Should().Be(expectedResult); [Fact] public void GetDataType_UnsupportedType_ShouldThrow() => Invoking(() => this.adapter.GetDataType(typeof(Entity), EnumSerializationMode.Strings)) - .Should().Throw() + .Should() + .Throw() .WithMessage($"Could not map the type {typeof(Entity)} to an SQLite data type.*"); [Fact] public void QuoteIdentifier_ShouldQuoteIdentifier() => - this.adapter.QuoteIdentifier("MyTable") - .Should().Be("\"MyTable\""); + this.adapter.QuoteIdentifier("MyTable").Should().Be("\"MyTable\""); [Fact] public void QuoteTemporaryTableName_ShouldQuoteTableName() => - this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection) - .Should().Be("temp.\"TempTable\""); + this.adapter.QuoteTemporaryTableName("TempTable", this.MockDbConnection).Should().Be("temp.\"TempTable\""); [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.adapter.BindParameterValue(Substitute.For(), null) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.BindParameterValue(Substitute.For(), null)); ArgumentNullGuardVerifier.Verify(() => this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None) ); - ArgumentNullGuardVerifier.Verify(() => - this.adapter.GetDataType(typeof(Int32), EnumSerializationMode.Integers) - ); + ArgumentNullGuardVerifier.Verify(() => this.adapter.GetDataType(typeof(int), EnumSerializationMode.Integers)); } [Fact] public void TemporaryTableBuilder_ShouldReturnBuilder() => - this.adapter.TemporaryTableBuilder - .Should().BeOfType(); + this.adapter.TemporaryTableBuilder.Should().BeOfType(); [Fact] public void WasSqlStatementCancelledByCancellationToken_ShouldAlwaysReturnFalse() => - this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None) - .Should().BeFalse(); - - private readonly SqliteDatabaseAdapter adapter = new(); + this.adapter.WasSqlStatementCancelledByCancellationToken(new(), CancellationToken.None).Should().BeFalse(); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilderTests.cs index ef3474e..fef8d45 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/Sqlite/SqliteTemporaryTableBuilderTests.cs @@ -4,49 +4,47 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters.Sqlite; public class SqliteTemporaryTableBuilderTests : UnitTestsBase { + private readonly SqliteTemporaryTableBuilder builder = new(new()); + [Fact] - public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); - Invoking(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) + await Invoking(() => + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int)) ) - .Should().Throw(); + .Should() + .ThrowAsync(); } [Fact] - public async Task BuildTemporaryTableAsync_NameIsNullOrEmptyOrWhitespace_ShouldThrow() + public void BuildTemporaryTable_NameIsNullOrEmptyOrWhitespace_ShouldThrow() { - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, "", new[] { 1 }, typeof(int))) + .Should() + .Throw(); - await Invoking(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, " ", new[] { 1 }, typeof(Int32)) - ) - .Should().ThrowAsync(); + Invoking(() => this.builder.BuildTemporaryTable(this.MockDbConnection, null, " ", new[] { 1 }, typeof(int))) + .Should() + .Throw(); } [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - new SqliteTemporaryTableBuilder(new()) - ); + ArgumentNullGuardVerifier.Verify(() => new SqliteTemporaryTableBuilder(new())); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTable(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); ArgumentNullGuardVerifier.Verify(() => - this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(Int32)) + this.builder.BuildTemporaryTableAsync(this.MockDbConnection, null, "Name", new[] { 1 }, typeof(int)) ); } - - private readonly SqliteTemporaryTableBuilder builder = new(new()); } diff --git a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/TemporaryTableDisposerTests.cs b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/TemporaryTableDisposerTests.cs index d26d06e..6363efc 100644 --- a/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/TemporaryTableDisposerTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DatabaseAdapters/TemporaryTableDisposerTests.cs @@ -5,58 +5,58 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DatabaseAdapters; public class TemporaryTableDisposerTests : UnitTestsBase { [Fact] - public void Dispose_AlreadyDisposed_ShouldNotCallDropFunctionAgain() + public async Task DisposeAsync_AlreadyDisposed_ShouldNotCallAsyncDropFunctionAgain() { var dropTableFunction = Substitute.For(); var dropTableAsyncFunction = Substitute.For>(); var disposer = new TemporaryTableDisposer(dropTableFunction, dropTableAsyncFunction); - disposer.Dispose(); - disposer.Dispose(); - disposer.Dispose(); + await disposer.DisposeAsync(); + await disposer.DisposeAsync(); + await disposer.DisposeAsync(); - dropTableFunction.Received(1).Invoke(); + await dropTableAsyncFunction.Received(1).Invoke(); } [Fact] - public void Dispose_ShouldCallDropFunction() + public async Task DisposeAsync_ShouldCallAsyncDropFunction() { var dropTableFunction = Substitute.For(); var dropTableAsyncFunction = Substitute.For>(); var disposer = new TemporaryTableDisposer(dropTableFunction, dropTableAsyncFunction); - disposer.Dispose(); - dropTableFunction.Received(1).Invoke(); + await disposer.DisposeAsync(); + + await dropTableAsyncFunction.Received(1).Invoke(); } [Fact] - public async Task DisposeAsync_AlreadyDisposed_ShouldNotCallAsyncDropFunctionAgain() + public void Dispose_AlreadyDisposed_ShouldNotCallDropFunctionAgain() { var dropTableFunction = Substitute.For(); var dropTableAsyncFunction = Substitute.For>(); var disposer = new TemporaryTableDisposer(dropTableFunction, dropTableAsyncFunction); - await disposer.DisposeAsync(); - await disposer.DisposeAsync(); - await disposer.DisposeAsync(); + disposer.Dispose(); + disposer.Dispose(); + disposer.Dispose(); - await dropTableAsyncFunction.Received(1).Invoke(); + dropTableFunction.Received(1).Invoke(); } [Fact] - public async Task DisposeAsync_ShouldCallAsyncDropFunction() + public void Dispose_ShouldCallDropFunction() { var dropTableFunction = Substitute.For(); var dropTableAsyncFunction = Substitute.For>(); var disposer = new TemporaryTableDisposer(dropTableFunction, dropTableAsyncFunction); + disposer.Dispose(); - await disposer.DisposeAsync(); - - await dropTableAsyncFunction.Received(1).Invoke(); + dropTableFunction.Received(1).Invoke(); } [Fact] @@ -65,8 +65,6 @@ public void ShouldGuardAgainstNullArguments() Action dropTableFunction = () => { }; Func dropTableAsyncFunction = () => ValueTask.CompletedTask; - ArgumentNullGuardVerifier.Verify(() => - new TemporaryTableDisposer(dropTableFunction, dropTableAsyncFunction) - ); + ArgumentNullGuardVerifier.Verify(() => new TemporaryTableDisposer(dropTableFunction, dropTableAsyncFunction)); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs index c4330ea..fc45de3 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandBuilderTests.cs @@ -9,10 +9,13 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DbCommands; public class DbCommandBuilderTests : UnitTestsBase { + private readonly List testEntityIds = Generate.Ids(); + private readonly long testProductId = Generate.Id(); + [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_CancellationToken_ShouldUseCancellationToken(Boolean useAsyncApi) + public async Task BuildDbCommand_CancellationToken_ShouldUseCancellationToken(bool useAsyncApi) { using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; @@ -33,7 +36,7 @@ public async Task BuildDbCommand_CancellationToken_ShouldUseCancellationToken(Bo [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_Code_Parameters_ShouldStoreCodeAndParameters(Boolean useAsyncApi) + public async Task BuildDbCommand_Code_Parameters_ShouldStoreCodeAndParameters(bool useAsyncApi) { var statement = new InterpolatedSqlStatement( "Code", @@ -42,42 +45,29 @@ public async Task BuildDbCommand_Code_Parameters_ShouldStoreCodeAndParameters(Bo ("Parameter3", "Value3") ); - var (command, _) = await CallApi( - useAsyncApi, - statement, - this.MockDatabaseAdapter, - this.MockDbConnection - ); + var (command, _) = await CallApi(useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection); - command.CommandText - .Should().Be("Code"); + command.CommandText.Should().Be("Code"); - command.Parameters.Count - .Should().Be(3); + command.Parameters.Count.Should().Be(3); - command.Parameters[0].ParameterName - .Should().Be("Parameter1"); + command.Parameters[0].ParameterName.Should().Be("Parameter1"); - command.Parameters[0].Value - .Should().Be("Value1"); + command.Parameters[0].Value.Should().Be("Value1"); - command.Parameters[1].ParameterName - .Should().Be("Parameter2"); + command.Parameters[1].ParameterName.Should().Be("Parameter2"); - command.Parameters[1].Value - .Should().Be("Value2"); + command.Parameters[1].Value.Should().Be("Value2"); - command.Parameters[2].ParameterName - .Should().Be("Parameter3"); + command.Parameters[2].ParameterName.Should().Be("Parameter3"); - command.Parameters[2].Value - .Should().Be("Value3"); + command.Parameters[2].Value.Should().Be("Value3"); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_CommandTimeout_ShouldUseCommandTimeout(Boolean useAsyncApi) + public async Task BuildDbCommand_CommandTimeout_ShouldUseCommandTimeout(bool useAsyncApi) { var timeout = Generate.Single(); @@ -89,14 +79,13 @@ public async Task BuildDbCommand_CommandTimeout_ShouldUseCommandTimeout(Boolean commandTimeout: timeout ); - command.CommandTimeout - .Should().Be((Int32)timeout.TotalSeconds); + command.CommandTimeout.Should().Be((int)timeout.TotalSeconds); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_CommandType_ShouldUseCommandType(Boolean useAsyncApi) + public async Task BuildDbCommand_CommandType_ShouldUseCommandType(bool useAsyncApi) { var (command, _) = await CallApi( useAsyncApi, @@ -106,14 +95,13 @@ public async Task BuildDbCommand_CommandType_ShouldUseCommandType(Boolean useAsy commandType: CommandType.StoredProcedure ); - command.CommandType - .Should().Be(CommandType.StoredProcedure); + command.CommandType.Should().Be(CommandType.StoredProcedure); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_InterpolatedParameter_DuplicateName_ShouldAppendSuffix(Boolean useAsyncApi) + public async Task BuildDbCommand_InterpolatedParameter_DuplicateName_ShouldAppendSuffix(bool useAsyncApi) { var value = Generate.ScalarValue(); @@ -124,20 +112,21 @@ public async Task BuildDbCommand_InterpolatedParameter_DuplicateName_ShouldAppen this.MockDbConnection ); - command.CommandText - .Should().Be("SELECT @Value, @Value2, @Value3, @Value4, @Value5"); + command.CommandText.Should().Be("SELECT @Value, @Value2, @Value3, @Value4, @Value5"); - command.Parameters.OfType().Select(a => a.ParameterName) - .Should().BeEquivalentTo("Value", "Value2", "Value3", "Value4", "Value5"); + command + .Parameters.OfType() + .Select(a => a.ParameterName) + .Should() + .BeEquivalentTo("Value", "Value2", "Value3", "Value4", "Value5"); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger( - Boolean useAsyncApi - ) + public async Task BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; @@ -150,23 +139,19 @@ Boolean useAsyncApi this.MockDbConnection ); - command.Parameters.Count - .Should().Be(1); + command.Parameters.Count.Should().Be(1); - command.Parameters[0].ParameterName - .Should().Be("EnumValue"); + command.Parameters[0].ParameterName.Should().Be("EnumValue"); - command.Parameters[0].Value - .Should().Be((Int32)enumValue); + command.Parameters[0].Value.Should().Be((int)enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString( - Boolean useAsyncApi - ) + public async Task BuildDbCommand_InterpolatedParameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; @@ -179,25 +164,22 @@ Boolean useAsyncApi this.MockDbConnection ); - command.Parameters.Count - .Should().Be(1); + command.Parameters.Count.Should().Be(1); - command.Parameters[0].ParameterName - .Should().Be("EnumValue"); + command.Parameters[0].ParameterName.Should().Be("EnumValue"); - command.Parameters[0].Value - .Should().Be(enumValue.ToString()); + command.Parameters[0].Value.Should().Be(enumValue.ToString()); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_InterpolatedParameter_ShouldHandleNullAndNonNullValues(Boolean useAsyncApi) + public async Task BuildDbCommand_InterpolatedParameter_ShouldHandleNullAndNonNullValues(bool useAsyncApi) { - Int64? id1 = Generate.Id(); - Int64? id2 = null; - Object value1 = Generate.Single(); - Object? value2 = null; + long? id1 = Generate.Id(); + long? id2 = null; + object value1 = Generate.Single(); + object? value2 = null; var (command, _) = await CallApi( useAsyncApi, @@ -206,49 +188,39 @@ public async Task BuildDbCommand_InterpolatedParameter_ShouldHandleNullAndNonNul this.MockDbConnection ); - command.Parameters.Count - .Should().Be(4); + command.Parameters.Count.Should().Be(4); - command.CommandText - .Should().Be("SELECT @Id1, @Id2, @Value1, @Value2"); + command.CommandText.Should().Be("SELECT @Id1, @Id2, @Value1, @Value2"); - command.Parameters[0].ParameterName - .Should().Be("Id1"); + command.Parameters[0].ParameterName.Should().Be("Id1"); - command.Parameters[0].Value - .Should().Be(id1); + command.Parameters[0].Value.Should().Be(id1); - command.Parameters[1].ParameterName - .Should().Be("Id2"); + command.Parameters[1].ParameterName.Should().Be("Id2"); - command.Parameters[1].Value - .Should().Be(DBNull.Value); + command.Parameters[1].Value.Should().Be(DBNull.Value); - command.Parameters[2].ParameterName - .Should().Be("Value1"); + command.Parameters[2].ParameterName.Should().Be("Value1"); - command.Parameters[2].Value - .Should().Be(value1); + command.Parameters[2].Value.Should().Be(value1); - command.Parameters[3].ParameterName - .Should().Be("Value2"); + command.Parameters[3].ParameterName.Should().Be("Value2"); - command.Parameters[3].Value - .Should().Be(DBNull.Value); + command.Parameters[3].Value.Should().Be(DBNull.Value); } [Theory] [InlineData(false)] [InlineData(true)] public async Task BuildDbCommand_InterpolatedParameter_ShouldInferNameFromValueExpressionIfPossible( - Boolean useAsyncApi + bool useAsyncApi ) { var productId = Generate.Id(); - static Int64 GetProductId() => Generate.Id(); + static long GetProductId() => Generate.Id(); #pragma warning disable RCS1163 // Unused parameter #pragma warning disable IDE0060 // Remove unused parameter - static Int64 GetProductIdByCategory(String category) => Generate.Id(); + static long GetProductIdByCategory(string category) => Generate.Id(); #pragma warning restore IDE0060 // Remove unused parameter #pragma warning restore RCS1163 // Unused parameter var productIds = Generate.Ids().ToArray(); @@ -256,19 +228,20 @@ Boolean useAsyncApi var (command, _) = await CallApi( useAsyncApi, $""" - SELECT {Parameter(productId)}, - {Parameter(GetProductId())}, - {Parameter(GetProductIdByCategory("Shoes"))}, - {Parameter(productIds[1])}, - {Parameter(this.testProductId)}, - {Parameter(new { })} - """, + SELECT {Parameter(productId)}, + {Parameter(GetProductId())}, + {Parameter(GetProductIdByCategory("Shoes"))}, + {Parameter(productIds[1])}, + {Parameter(this.testProductId)}, + {Parameter(new { })} + """, this.MockDatabaseAdapter, this.MockDbConnection ); - command.CommandText - .Should().Be( + command + .CommandText.Should() + .Be( """ SELECT @ProductId, @ProductId2, @@ -279,32 +252,25 @@ Boolean useAsyncApi """ ); - command.Parameters.Count - .Should().Be(6); + command.Parameters.Count.Should().Be(6); - command.Parameters[0].ParameterName - .Should().Be("ProductId"); + command.Parameters[0].ParameterName.Should().Be("ProductId"); - command.Parameters[1].ParameterName - .Should().Be("ProductId2"); + command.Parameters[1].ParameterName.Should().Be("ProductId2"); - command.Parameters[2].ParameterName - .Should().Be("ProductIdByCategoryShoes"); + command.Parameters[2].ParameterName.Should().Be("ProductIdByCategoryShoes"); - command.Parameters[3].ParameterName - .Should().Be("ProductIds1"); + command.Parameters[3].ParameterName.Should().Be("ProductIds1"); - command.Parameters[4].ParameterName - .Should().Be("TestProductId"); + command.Parameters[4].ParameterName.Should().Be("TestProductId"); - command.Parameters[5].ParameterName - .Should().Be("Parameter_6"); + command.Parameters[5].ParameterName.Should().Be("Parameter_6"); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_InterpolatedParameter_ShouldStoreParameter(Boolean useAsyncApi) + public async Task BuildDbCommand_InterpolatedParameter_ShouldStoreParameter(bool useAsyncApi) { var value = Generate.ScalarValue(); @@ -315,235 +281,198 @@ public async Task BuildDbCommand_InterpolatedParameter_ShouldStoreParameter(Bool this.MockDbConnection ); - command.CommandText - .Should().Be("SELECT @Value"); + command.CommandText.Should().Be("SELECT @Value"); - command.Parameters.Count - .Should().Be(1); + command.Parameters.Count.Should().Be(1); - command.Parameters[0].ParameterName - .Should().Be("Value"); + command.Parameters[0].ParameterName.Should().Be("Value"); - command.Parameters[0].Value - .Should().Be(value); + command.Parameters[0].Value.Should().Be(value); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_InterpolatedParameter_ShouldSupportComplexExpressions(Boolean useAsyncApi) + public async Task BuildDbCommand_InterpolatedParameter_ShouldSupportComplexExpressions(bool useAsyncApi) { - const Double baseDiscount = 0.1; + const double baseDiscount = 0.1; var entityIds = Generate.Ids(20); var (command, _) = await CallApi( useAsyncApi, $""" - SELECT {Parameter(baseDiscount * 5 / 3)}, - {Parameter(entityIds.Where(a => a > 5).ToArray()[0])} - """, + SELECT {Parameter(baseDiscount * 5 / 3)}, + {Parameter(entityIds.Where(a => a > 5).ToArray()[0])} + """, this.MockDatabaseAdapter, this.MockDbConnection ); - command.CommandText - .Should().Be( + command + .CommandText.Should() + .Be( """ SELECT @BaseDiscount53, @EntityIdsWhereaa5ToArray0 """ ); - command.Parameters.Count - .Should().Be(2); + command.Parameters.Count.Should().Be(2); - command.Parameters[0].ParameterName - .Should().Be("BaseDiscount53"); + command.Parameters[0].ParameterName.Should().Be("BaseDiscount53"); - command.Parameters[0].Value - .Should().Be(baseDiscount * 5 / 3); + command.Parameters[0].Value.Should().Be(baseDiscount * 5 / 3); - command.Parameters[1].ParameterName - .Should().Be("EntityIdsWhereaa5ToArray0"); + command.Parameters[1].ParameterName.Should().Be("EntityIdsWhereaa5ToArray0"); - command.Parameters[1].Value - .Should().Be(entityIds.Where(a => a > 5).ToArray()[0]); + command.Parameters[1].Value.Should().Be(entityIds.Where(a => a > 5).ToArray()[0]); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildDbCommand_InterpolatedTemporaryTable_DatabaseAdapterDoesNotSupportTemporaryTables_ShouldThrow( - Boolean useAsyncApi - ) + public async Task BuildDbCommand_InterpolatedTemporaryTable_DatabaseAdapterDoesNotSupportTemporaryTables_ShouldThrow( + bool useAsyncApi + ) { var entityIds = Generate.Ids(); this.MockDatabaseAdapter.SupportsTemporaryTables(Arg.Any()).Returns(false); - await Invoking(() => CallApi( + await Invoking(() => + CallApi( useAsyncApi, $"SELECT Value FROM {TemporaryTable(entityIds)}", this.MockDatabaseAdapter, this.MockDbConnection ) ) - .Should().ThrowAsync() + .Should() + .ThrowAsync() .WithMessage( - $"The database adapter {this.MockDatabaseAdapter.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 {this.MockDatabaseAdapter.GetType()} does not support " + + "(local / session-scoped) temporary tables. Therefore the temporary tables feature of " + + "DbConnectionPlus can not be used with this database." ); - // No temporary table used - should not throw. - await Invoking(() => CallApi( - useAsyncApi, - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection - ) - ) - .Should().NotThrowAsync(); + await Invoking(() => CallApi(useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection)) + .Should() + .NotThrowAsync(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildDbCommand_InterpolatedTemporaryTable_ShouldInferTableNameFromValuesExpressionIfPossible( - Boolean useAsyncApi - ) + public async Task BuildDbCommand_InterpolatedTemporaryTable_ShouldInferTableNameFromValuesExpressionIfPossible( + bool useAsyncApi + ) { var entityIds = Generate.Ids(); - static List Get() => Generate.Ids(); - static List GetEntityIds() => Generate.Ids(); + static List Get() => Generate.Ids(); + static List GetEntityIds() => Generate.Ids(); #pragma warning disable RCS1163 // Unused parameter #pragma warning disable IDE0060 // Remove unused parameter - static List GetEntityIdsByCategory(String category) => Generate.Ids(); + static List GetEntityIdsByCategory(string category) => Generate.Ids(); #pragma warning restore IDE0060 // Remove unused parameter #pragma warning restore RCS1163 // Unused parameter - InterpolatedSqlStatement statement = - $""" - SELECT Value FROM {TemporaryTable(entityIds)} - UNION - SELECT Value FROM {TemporaryTable(GetEntityIds())} - UNION - SELECT Value FROM {TemporaryTable(GetEntityIdsByCategory("Shoes"))} - UNION - SELECT Value FROM {TemporaryTable(this.testEntityIds)} - UNION - SELECT Value FROM {TemporaryTable(Get())} - """; + InterpolatedSqlStatement statement = $""" + SELECT Value FROM {TemporaryTable(entityIds)} + UNION + SELECT Value FROM {TemporaryTable(GetEntityIds())} + UNION + SELECT Value FROM {TemporaryTable(GetEntityIdsByCategory("Shoes"))} + UNION + SELECT Value FROM {TemporaryTable(this.testEntityIds)} + UNION + SELECT Value FROM {TemporaryTable(Get())} + """; - var (command, _) = await CallApi( - useAsyncApi, - statement, - this.MockDatabaseAdapter, - this.MockDbConnection - ); + var (command, _) = await CallApi(useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection); var temporaryTables = statement.TemporaryTables; - temporaryTables - .Should().HaveCount(5); + temporaryTables.Should().HaveCount(5); - command.CommandText - .Should().Be( + command + .CommandText.Should() + .Be( $""" - SELECT Value FROM [#{temporaryTables[0].Name}] - UNION - SELECT Value FROM [#{temporaryTables[1].Name}] - UNION - SELECT Value FROM [#{temporaryTables[2].Name}] - UNION - SELECT Value FROM [#{temporaryTables[3].Name}] - UNION - SELECT Value FROM [#{temporaryTables[4].Name}] - """ + SELECT Value FROM [#{temporaryTables[0].Name}] + UNION + SELECT Value FROM [#{temporaryTables[1].Name}] + UNION + SELECT Value FROM [#{temporaryTables[2].Name}] + UNION + SELECT Value FROM [#{temporaryTables[3].Name}] + UNION + SELECT Value FROM [#{temporaryTables[4].Name}] + """ ); - temporaryTables[0].Name - .Should().StartWith("EntityIds_"); + temporaryTables[0].Name.Should().StartWith("EntityIds_"); - temporaryTables[1].Name - .Should().StartWith("EntityIds_"); + temporaryTables[1].Name.Should().StartWith("EntityIds_"); - temporaryTables[2].Name - .Should().StartWith("EntityIdsByCategoryShoes_"); + temporaryTables[2].Name.Should().StartWith("EntityIdsByCategoryShoes_"); - temporaryTables[3].Name - .Should().StartWith("TestEntityIds_"); + temporaryTables[3].Name.Should().StartWith("TestEntityIds_"); - temporaryTables[4].Name - .Should().StartWith("Values_"); + temporaryTables[4].Name.Should().StartWith("Values_"); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_InterpolatedTemporaryTable_ShouldStoreTemporaryTable(Boolean useAsyncApi) + public async Task BuildDbCommand_InterpolatedTemporaryTable_ShouldStoreTemporaryTable(bool useAsyncApi) { var entities = Generate.Multiple(); var entityIds = Generate.Ids(); - InterpolatedSqlStatement statement = - $""" - SELECT Id - FROM {TemporaryTable(entities)} Entities - WHERE Entities.Id IN (SELECT Value FROM {TemporaryTable(entityIds)}) - """; + InterpolatedSqlStatement statement = $""" + SELECT Id + FROM {TemporaryTable(entities)} Entities + WHERE Entities.Id IN (SELECT Value FROM {TemporaryTable(entityIds)}) + """; - var (command, _) = await CallApi( - useAsyncApi, - statement, - this.MockDatabaseAdapter, - this.MockDbConnection - ); + var (command, _) = await CallApi(useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection); var temporaryTables = statement.TemporaryTables; - temporaryTables - .Should().HaveCount(2); + temporaryTables.Should().HaveCount(2); var table1 = temporaryTables[0]; - table1.Name - .Should().StartWith("Entities_"); + table1.Name.Should().StartWith("Entities_"); - table1.Values - .Should().Be(entities); + table1.Values.Should().Be(entities); - table1.ValuesType - .Should().Be(typeof(Entity)); + table1.ValuesType.Should().Be(typeof(Entity)); var table2 = temporaryTables[1]; - table2.Name - .Should().StartWith("EntityIds_"); + table2.Name.Should().StartWith("EntityIds_"); - table2.Values - .Should().BeEquivalentTo(entityIds); + table2.Values.Should().BeEquivalentTo(entityIds); - table2.ValuesType - .Should().Be(typeof(Int64)); + table2.ValuesType.Should().Be(typeof(long)); - command.CommandText - .Should().Be( + command + .CommandText.Should() + .Be( $""" - SELECT Id - FROM [#{table1.Name}] Entities - WHERE Entities.Id IN (SELECT Value FROM [#{table2.Name}]) - """ + SELECT Id + FROM [#{table1.Name}] Entities + WHERE Entities.Id IN (SELECT Value FROM [#{table2.Name}]) + """ ); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_MultipleInterpolatedParameters_ShouldStoreParameters(Boolean useAsyncApi) + public async Task BuildDbCommand_MultipleInterpolatedParameters_ShouldStoreParameters(bool useAsyncApi) { var value1 = Generate.ScalarValue(); var value2 = Generate.ScalarValue(); @@ -556,120 +485,86 @@ public async Task BuildDbCommand_MultipleInterpolatedParameters_ShouldStoreParam this.MockDbConnection ); - command.CommandText - .Should() - .Be("SELECT @Value1, @Value2, @Value3"); + command.CommandText.Should().Be("SELECT @Value1, @Value2, @Value3"); - command.Parameters.Count - .Should().Be(3); + command.Parameters.Count.Should().Be(3); - command.Parameters[0].ParameterName - .Should().Be("Value1"); + command.Parameters[0].ParameterName.Should().Be("Value1"); - command.Parameters[0].Value - .Should().Be(value1); + command.Parameters[0].Value.Should().Be(value1); - command.Parameters[1].ParameterName - .Should().Be("Value2"); + command.Parameters[1].ParameterName.Should().Be("Value2"); - command.Parameters[1].Value - .Should().Be(value2); + command.Parameters[1].Value.Should().Be(value2); - command.Parameters[2].ParameterName - .Should().Be("Value3"); + command.Parameters[2].ParameterName.Should().Be("Value3"); - command.Parameters[2].Value - .Should().Be(value3); + command.Parameters[2].Value.Should().Be(value3); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger( - Boolean useAsyncApi - ) + public async Task BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsIntegers_ShouldSerializeEnumToInteger( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Integers; var enumValue = Generate.Single(); - var statement = new InterpolatedSqlStatement( - "Code", - ("Parameter1", enumValue) - ); + var statement = new InterpolatedSqlStatement("Code", ("Parameter1", enumValue)); - var (command, _) = await CallApi( - useAsyncApi, - statement, - this.MockDatabaseAdapter, - this.MockDbConnection - ); + var (command, _) = await CallApi(useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection); - command.Parameters.Count - .Should().Be(1); + command.Parameters.Count.Should().Be(1); - command.Parameters[0].ParameterName - .Should().Be("Parameter1"); + command.Parameters[0].ParameterName.Should().Be("Parameter1"); - command.Parameters[0].Value - .Should().Be((Int32)enumValue); + command.Parameters[0].Value.Should().Be((int)enumValue); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task - BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString( - Boolean useAsyncApi - ) + public async Task BuildDbCommand_Parameter_EnumValue_EnumSerializationModeIsStrings_ShouldSerializeEnumToString( + bool useAsyncApi + ) { DbConnectionPlusConfiguration.Instance.EnumSerializationMode = EnumSerializationMode.Strings; var enumValue = Generate.Single(); - var statement = new InterpolatedSqlStatement( - "Code", - ("Parameter1", enumValue) - ); + var statement = new InterpolatedSqlStatement("Code", ("Parameter1", enumValue)); - var (command, _) = await CallApi( - useAsyncApi, - statement, - this.MockDatabaseAdapter, - this.MockDbConnection - ); + var (command, _) = await CallApi(useAsyncApi, statement, this.MockDatabaseAdapter, this.MockDbConnection); - command.Parameters.Count - .Should().Be(1); + command.Parameters.Count.Should().Be(1); - command.Parameters[0].ParameterName - .Should().Be("Parameter1"); + command.Parameters[0].ParameterName.Should().Be("Parameter1"); - command.Parameters[0].Value - .Should().Be(enumValue.ToString()); + command.Parameters[0].Value.Should().Be(enumValue.ToString()); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldFormatAndStoreLiteral(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldFormatAndStoreLiteral(bool useAsyncApi) { var (command, _) = await CallApi( useAsyncApi, - $"SELECT {123.45,10:N2}, {123.45,-10:N2}", + $"SELECT {123.45, 10:N2}, {123.45, -10:N2}", this.MockDatabaseAdapter, this.MockDbConnection ); - command.CommandText - .Should().Be("SELECT 123.45, 123.45 "); + command.CommandText.Should().Be("SELECT 123.45, 123.45 "); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldReturnCommandDisposer(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldReturnCommandDisposer(bool useAsyncApi) { var (_, commandDisposer) = await CallApi( useAsyncApi, @@ -678,30 +573,23 @@ public async Task BuildDbCommand_ShouldReturnCommandDisposer(Boolean useAsyncApi this.MockDbConnection ); - commandDisposer - .Should().NotBeNull(); + commandDisposer.Should().NotBeNull(); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_ShouldStoreLiteral(Boolean useAsyncApi) + public async Task BuildDbCommand_ShouldStoreLiteral(bool useAsyncApi) { - var (command, _) = await CallApi( - useAsyncApi, - "SELECT 1", - this.MockDatabaseAdapter, - this.MockDbConnection - ); + var (command, _) = await CallApi(useAsyncApi, "SELECT 1", this.MockDatabaseAdapter, this.MockDbConnection); - command.CommandText - .Should().Be("SELECT 1"); + command.CommandText.Should().Be("SELECT 1"); } [Theory] [InlineData(false)] [InlineData(true)] - public async Task BuildDbCommand_Transaction_ShouldUseTransaction(Boolean useAsyncApi) + public async Task BuildDbCommand_Transaction_ShouldUseTransaction(bool useAsyncApi) { await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); @@ -713,12 +601,11 @@ public async Task BuildDbCommand_Transaction_ShouldUseTransaction(Boolean useAsy transaction ); - command.Transaction - .Should().BeSameAs(transaction); + command.Transaction.Should().BeSameAs(transaction); } private static Task<(DbCommand, DbCommandDisposer)> CallApi( - Boolean useAsyncApi, + bool useAsyncApi, InterpolatedSqlStatement statement, IDatabaseAdapter databaseAdapter, DbConnection connection, @@ -760,7 +647,4 @@ public async Task BuildDbCommand_Transaction_ShouldUseTransaction(Boolean useAsy return Task.FromException<(DbCommand, DbCommandDisposer)>(ex); } } - - private readonly List testEntityIds = Generate.Ids(); - private readonly Int64 testProductId = Generate.Id(); } diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandDisposerTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandDisposerTests.cs index 613f6dd..424374c 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandDisposerTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandDisposerTests.cs @@ -8,12 +8,14 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.DbCommands; public class DbCommandDisposerTests : UnitTestsBase { [Fact] - public void Dispose_AlreadyDisposed_ShouldNotDisposeCommandResourcesAgain() + public async Task DisposeAsync_AlreadyDisposed_ShouldNotDisposeCommandResourcesAsyncAgain() { using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(this.MockDbCommand, cancellationToken); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + this.MockDbCommand, + cancellationToken + ); var dropTableFunction1 = Substitute.For(); var dropTableAsyncFunction1 = Substitute.For>(); @@ -24,7 +26,7 @@ public void Dispose_AlreadyDisposed_ShouldNotDisposeCommandResourcesAgain() var temporaryTableDisposers = new[] { new TemporaryTableDisposer(dropTableFunction1, dropTableAsyncFunction1), - new TemporaryTableDisposer(dropTableFunction2, dropTableAsyncFunction2) + new TemporaryTableDisposer(dropTableFunction2, dropTableAsyncFunction2), }; var disposer = new DbCommandDisposer( @@ -33,22 +35,24 @@ public void Dispose_AlreadyDisposed_ShouldNotDisposeCommandResourcesAgain() cancellationTokenRegistration ); - disposer.Dispose(); - disposer.Dispose(); - disposer.Dispose(); + await disposer.DisposeAsync(); + await disposer.DisposeAsync(); + await disposer.DisposeAsync(); - this.MockDbCommand.Received(1).Dispose(); - dropTableFunction1.Received(1).Invoke(); - dropTableFunction2.Received(1).Invoke(); + await this.MockDbCommand.Received(1).DisposeAsync(); + await dropTableAsyncFunction1.Received(1).Invoke(); + await dropTableAsyncFunction2.Received(1).Invoke(); } [Fact] - public void Dispose_ShouldDisposeCommandResources() + public async Task DisposeAsync_ShouldDisposeCommandResourcesAsync() { using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(this.MockDbCommand, cancellationToken); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + this.MockDbCommand, + cancellationToken + ); var dropTableFunction1 = Substitute.For(); var dropTableAsyncFunction1 = Substitute.For>(); @@ -59,7 +63,7 @@ public void Dispose_ShouldDisposeCommandResources() var temporaryTableDisposers = new[] { new TemporaryTableDisposer(dropTableFunction1, dropTableAsyncFunction1), - new TemporaryTableDisposer(dropTableFunction2, dropTableAsyncFunction2) + new TemporaryTableDisposer(dropTableFunction2, dropTableAsyncFunction2), }; var disposer = new DbCommandDisposer( @@ -68,24 +72,26 @@ public void Dispose_ShouldDisposeCommandResources() cancellationTokenRegistration ); - disposer.Dispose(); + await disposer.DisposeAsync(); - this.MockDbCommand.Received(1).Dispose(); - dropTableFunction1.Received(1).Invoke(); - dropTableFunction2.Received(1).Invoke(); + await this.MockDbCommand.Received(1).DisposeAsync(); + await dropTableAsyncFunction1.Received(1).Invoke(); + await dropTableAsyncFunction2.Received(1).Invoke(); // Verify that cancellation token registration is disposed. - cancellationTokenSource.Cancel(); + await cancellationTokenSource.CancelAsync(); this.MockDbCommand.DidNotReceive().Cancel(); } [Fact] - public async Task DisposeAsync_AlreadyDisposed_ShouldNotDisposeCommandResourcesAsyncAgain() + public void Dispose_AlreadyDisposed_ShouldNotDisposeCommandResourcesAgain() { using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(this.MockDbCommand, cancellationToken); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + this.MockDbCommand, + cancellationToken + ); var dropTableFunction1 = Substitute.For(); var dropTableAsyncFunction1 = Substitute.For>(); @@ -96,7 +102,7 @@ public async Task DisposeAsync_AlreadyDisposed_ShouldNotDisposeCommandResourcesA var temporaryTableDisposers = new[] { new TemporaryTableDisposer(dropTableFunction1, dropTableAsyncFunction1), - new TemporaryTableDisposer(dropTableFunction2, dropTableAsyncFunction2) + new TemporaryTableDisposer(dropTableFunction2, dropTableAsyncFunction2), }; var disposer = new DbCommandDisposer( @@ -105,22 +111,24 @@ public async Task DisposeAsync_AlreadyDisposed_ShouldNotDisposeCommandResourcesA cancellationTokenRegistration ); - await disposer.DisposeAsync(); - await disposer.DisposeAsync(); - await disposer.DisposeAsync(); + disposer.Dispose(); + disposer.Dispose(); + disposer.Dispose(); - await this.MockDbCommand.Received(1).DisposeAsync(); - await dropTableAsyncFunction1.Received(1).Invoke(); - await dropTableAsyncFunction2.Received(1).Invoke(); + this.MockDbCommand.Received(1).Dispose(); + dropTableFunction1.Received(1).Invoke(); + dropTableFunction2.Received(1).Invoke(); } [Fact] - public async Task DisposeAsync_ShouldDisposeCommandResourcesAsync() + public void Dispose_ShouldDisposeCommandResources() { using var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var cancellationTokenRegistration = - DbCommandHelper.RegisterDbCommandCancellation(this.MockDbCommand, cancellationToken); + var cancellationTokenRegistration = DbCommandHelper.RegisterDbCommandCancellation( + this.MockDbCommand, + cancellationToken + ); var dropTableFunction1 = Substitute.For(); var dropTableAsyncFunction1 = Substitute.For>(); @@ -131,7 +139,7 @@ public async Task DisposeAsync_ShouldDisposeCommandResourcesAsync() var temporaryTableDisposers = new[] { new TemporaryTableDisposer(dropTableFunction1, dropTableAsyncFunction1), - new TemporaryTableDisposer(dropTableFunction2, dropTableAsyncFunction2) + new TemporaryTableDisposer(dropTableFunction2, dropTableAsyncFunction2), }; var disposer = new DbCommandDisposer( @@ -140,14 +148,14 @@ public async Task DisposeAsync_ShouldDisposeCommandResourcesAsync() cancellationTokenRegistration ); - await disposer.DisposeAsync(); + disposer.Dispose(); - await this.MockDbCommand.Received(1).DisposeAsync(); - await dropTableAsyncFunction1.Received(1).Invoke(); - await dropTableAsyncFunction2.Received(1).Invoke(); + this.MockDbCommand.Received(1).Dispose(); + dropTableFunction1.Received(1).Invoke(); + dropTableFunction2.Received(1).Invoke(); // Verify that cancellation token registration is disposed. - await cancellationTokenSource.CancelAsync(); + cancellationTokenSource.Cancel(); this.MockDbCommand.DidNotReceive().Cancel(); } @@ -156,11 +164,8 @@ public void ShouldGuardAgainstNullArguments() { TemporaryTableDisposer[] temporaryTableDisposers = []; - ArgumentNullGuardVerifier.Verify(() => new DbCommandDisposer( - this.MockDbCommand, - temporaryTableDisposers, - default - ) + ArgumentNullGuardVerifier.Verify(() => + new DbCommandDisposer(this.MockDbCommand, temporaryTableDisposers, default) ); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandHelperTests.cs b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandHelperTests.cs index f4e9eca..5158d39 100644 --- a/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbCommands/DbCommandHelperTests.cs @@ -12,11 +12,9 @@ public void RegisterDbCommandCancellation_CancellationToken_ShouldRegister() var registration = DbCommandHelper.RegisterDbCommandCancellation(this.MockDbCommand, cancellationToken); - registration - .Should().NotBe(default(CancellationTokenRegistration)); + registration.Should().NotBe(default(CancellationTokenRegistration)); - registration.Token - .Should().Be(cancellationToken); + registration.Token.Should().Be(cancellationToken); } [Fact] @@ -24,11 +22,9 @@ public void RegisterDbCommandCancellation_NoneCancellationToken_ShouldNotRegiste { var registration = DbCommandHelper.RegisterDbCommandCancellation(this.MockDbCommand, CancellationToken.None); - registration - .Should().Be(default(CancellationTokenRegistration)); + registration.Should().Be(default(CancellationTokenRegistration)); - registration.Token - .Should().Be(CancellationToken.None); + registration.Token.Should().Be(CancellationToken.None); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs index 3dce9a7..16c4161 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ConfigurationTests.cs @@ -8,113 +8,114 @@ public void Configure_ShouldConfigureDbConnectionPlus() InterceptDbCommand interceptDbCommand = (_, _) => { }; Configure(config => - { - config.EnumSerializationMode = EnumSerializationMode.Integers; - config.InterceptDbCommand = interceptDbCommand; - - config.Entity() - .ToTable("MappingTestEntity"); - - config.Entity() - .Property(a => a.Computed_) - .HasColumnName("Computed") - .IsComputed(); - - config.Entity() - .Property(a => a.ConcurrencyToken_) - .HasColumnName("ConcurrencyToken") - .IsConcurrencyToken(); - - config.Entity() - .Property(a => a.Identity_) - .HasColumnName("Identity") - .IsIdentity(); - - config.Entity() - .Property(a => a.Key1_) - .HasColumnName("Key1") - .IsKey(); - - config.Entity() - .Property(a => a.Key2_) - .HasColumnName("Key2") - .IsKey(); - - config.Entity() - .Property(a => a.Value_) - .HasColumnName("Value"); - - config.Entity() - .Property(a => a.NotMapped) - .IsIgnored(); - - config.Entity() - .Property(a => a.RowVersion_) - .HasColumnName("RowVersion") - .IsRowVersion(); - } - ); - - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - .Should().Be(EnumSerializationMode.Integers); - - DbConnectionPlusConfiguration.Instance.InterceptDbCommand - .Should().Be(interceptDbCommand); + { + config.EnumSerializationMode = EnumSerializationMode.Integers; + config.InterceptDbCommand = interceptDbCommand; + + config.Entity().ToTable("MappingTestEntity"); + + config + .Entity() + .Property(a => a.Computed_) + .HasColumnName("Computed") + .IsComputed(); + + config + .Entity() + .Property(a => a.ConcurrencyToken_) + .HasColumnName("ConcurrencyToken") + .IsConcurrencyToken(); + + config + .Entity() + .Property(a => a.Identity_) + .HasColumnName("Identity") + .IsIdentity(); + + config.Entity().Property(a => a.Key1_).HasColumnName("Key1").IsKey(); + + config.Entity().Property(a => a.Key2_).HasColumnName("Key2").IsKey(); + + config.Entity().Property(a => a.Value_).HasColumnName("Value"); + + config.Entity().Property(a => a.NotMapped).IsIgnored(); + + config + .Entity() + .Property(a => a.RowVersion_) + .HasColumnName("RowVersion") + .IsRowVersion(); + }); + + DbConnectionPlusConfiguration.Instance.EnumSerializationMode.Should().Be(EnumSerializationMode.Integers); + + DbConnectionPlusConfiguration.Instance.InterceptDbCommand.Should().Be(interceptDbCommand); var entityTypeBuilders = DbConnectionPlusConfiguration.Instance.GetEntityTypeBuilders(); - entityTypeBuilders - .Should().HaveCount(1); + entityTypeBuilders.Should().HaveCount(1); - entityTypeBuilders - .Should().ContainKeys( - typeof(MappingTestEntityFluentApi) - ); + entityTypeBuilders.Should().ContainKeys(typeof(MappingTestEntityFluentApi)); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].TableName - .Should().Be("MappingTestEntity"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].TableName.Should().Be("MappingTestEntity"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Computed_"].ColumnName - .Should().Be("Computed"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["Computed_"] + .ColumnName.Should() + .Be("Computed"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Computed_"].IsComputed - .Should().BeTrue(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["Computed_"] + .IsComputed.Should() + .BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ConcurrencyToken_"].ColumnName - .Should().Be("ConcurrencyToken"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["ConcurrencyToken_"] + .ColumnName.Should() + .Be("ConcurrencyToken"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["ConcurrencyToken_"].IsConcurrencyToken - .Should().BeTrue(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["ConcurrencyToken_"] + .IsConcurrencyToken.Should() + .BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Identity_"].ColumnName - .Should().Be("Identity"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["Identity_"] + .ColumnName.Should() + .Be("Identity"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Identity_"].IsIdentity - .Should().BeTrue(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["Identity_"] + .IsIdentity.Should() + .BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key1_"].ColumnName - .Should().Be("Key1"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key1_"].ColumnName.Should().Be("Key1"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key1_"].IsKey - .Should().BeTrue(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key1_"].IsKey.Should().BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key2_"].ColumnName - .Should().Be("Key2"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key2_"].ColumnName.Should().Be("Key2"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key2_"].IsKey - .Should().BeTrue(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Key2_"].IsKey.Should().BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["Value_"].ColumnName - .Should().Be("Value"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["Value_"] + .ColumnName.Should() + .Be("Value"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["NotMapped"].IsIgnored - .Should().BeTrue(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["NotMapped"] + .IsIgnored.Should() + .BeTrue(); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["RowVersion_"].ColumnName - .Should().Be("RowVersion"); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["RowVersion_"] + .ColumnName.Should() + .Be("RowVersion"); - entityTypeBuilders[typeof(MappingTestEntityFluentApi)].PropertyBuilders["RowVersion_"].IsRowVersion - .Should().BeTrue(); + entityTypeBuilders[typeof(MappingTestEntityFluentApi)] + .PropertyBuilders["RowVersion_"] + .IsRowVersion.Should() + .BeTrue(); } [Fact] @@ -123,7 +124,8 @@ public void Configure_ShouldFreezeConfiguration() Configure(configuration => configuration.EnumSerializationMode = EnumSerializationMode.Integers); Invoking(() => Configure(configuration => configuration.EnumSerializationMode = EnumSerializationMode.Strings)) - .Should().Throw() + .Should() + .Throw() .WithMessage("The configuration of DbConnectionPlus is frozen and can no longer be modified."); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.DeleteEntitiesTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.DeleteEntitiesTests.cs index 8adf1c0..899b180 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.DeleteEntitiesTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.DeleteEntitiesTests.cs @@ -3,55 +3,42 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_DeleteEntitiesTests : UnitTestsBase { [Fact] - public void DeleteEntities_ShouldCallEntityManipulator() + public async Task DeleteEntitiesAsync_ShouldCallEntityManipulator() { var entities = Generate.Multiple(); - using var transaction = this.MockDbConnection.BeginTransaction(); + await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.DeleteEntities( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.DeleteEntitiesAsync(this.MockDbConnection, entities, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - this.MockDbConnection.DeleteEntities(entities, transaction, cancellationToken) - .Should().Be(numberOfAffectedRows); + (await this.MockDbConnection.DeleteEntitiesAsync(entities, transaction, cancellationToken)) + .Should() + .Be(numberOfAffectedRows); - this.MockEntityManipulator.Received().DeleteEntities( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ); + await this + .MockEntityManipulator.Received() + .DeleteEntitiesAsync(this.MockDbConnection, entities, transaction, cancellationToken); } [Fact] - public async Task DeleteEntitiesAsync_ShouldCallEntityManipulator() + public void DeleteEntities_ShouldCallEntityManipulator() { var entities = Generate.Multiple(); - await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); + using var transaction = this.MockDbConnection.BeginTransaction(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.DeleteEntitiesAsync( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.DeleteEntities(this.MockDbConnection, entities, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - (await this.MockDbConnection.DeleteEntitiesAsync(entities, transaction, cancellationToken)) - .Should().Be(numberOfAffectedRows); + this.MockDbConnection.DeleteEntities(entities, transaction, cancellationToken) + .Should() + .Be(numberOfAffectedRows); - await this.MockEntityManipulator.Received().DeleteEntitiesAsync( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ); + this.MockEntityManipulator.Received() + .DeleteEntities(this.MockDbConnection, entities, transaction, cancellationToken); } [Fact] @@ -59,12 +46,8 @@ public void ShouldGuardAgainstNullArguments() { var entities = Generate.Multiple(); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.DeleteEntities(entities) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.DeleteEntities(entities)); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.DeleteEntitiesAsync(entities) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.DeleteEntitiesAsync(entities)); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.DeleteEntityTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.DeleteEntityTests.cs index 30bbb7c..2ce4383 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.DeleteEntityTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.DeleteEntityTests.cs @@ -3,55 +3,40 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_DeleteEntityTests : UnitTestsBase { [Fact] - public void DeleteEntity_ShouldCallEntityManipulator() + public async Task DeleteEntityAsync_ShouldCallEntityManipulator() { var entity = Generate.Single(); - using var transaction = this.MockDbConnection.BeginTransaction(); + await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.DeleteEntity( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.DeleteEntityAsync(this.MockDbConnection, entity, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - this.MockDbConnection.DeleteEntity(entity, transaction, cancellationToken) - .Should().Be(numberOfAffectedRows); + (await this.MockDbConnection.DeleteEntityAsync(entity, transaction, cancellationToken)) + .Should() + .Be(numberOfAffectedRows); - this.MockEntityManipulator.Received().DeleteEntity( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ); + await this + .MockEntityManipulator.Received() + .DeleteEntityAsync(this.MockDbConnection, entity, transaction, cancellationToken); } [Fact] - public async Task DeleteEntityAsync_ShouldCallEntityManipulator() + public void DeleteEntity_ShouldCallEntityManipulator() { var entity = Generate.Single(); - await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); + using var transaction = this.MockDbConnection.BeginTransaction(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.DeleteEntityAsync( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.DeleteEntity(this.MockDbConnection, entity, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - (await this.MockDbConnection.DeleteEntityAsync(entity, transaction, cancellationToken)) - .Should().Be(numberOfAffectedRows); + this.MockDbConnection.DeleteEntity(entity, transaction, cancellationToken).Should().Be(numberOfAffectedRows); - await this.MockEntityManipulator.Received().DeleteEntityAsync( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ); + this.MockEntityManipulator.Received() + .DeleteEntity(this.MockDbConnection, entity, transaction, cancellationToken); } [Fact] @@ -59,12 +44,8 @@ public void ShouldGuardAgainstNullArguments() { var entity = Generate.Single(); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.DeleteEntity(entity) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.DeleteEntity(entity)); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.DeleteEntityAsync(entity) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.DeleteEntityAsync(entity)); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteNonQueryTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteNonQueryTests.cs index 13713df..6a67eab 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteNonQueryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteNonQueryTests.cs @@ -1,35 +1,18 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; -public class DbConnectionExtensions_ExecuteNonQueryTests() : StatementMethodTestsBase( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.ExecuteNonQueryAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.ExecuteNonQuery(sql, transaction, timeout, commandType, cancellationToken) -) +public class DbConnectionExtensions_ExecuteNonQueryTests() + : StatementMethodTestsBase( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.ExecuteNonQueryAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.ExecuteNonQuery(sql, transaction, timeout, commandType, cancellationToken) + ) { [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.ExecuteNonQuery("DELETE FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.ExecuteNonQuery("DELETE FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.ExecuteNonQueryAsync("DELETE FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.ExecuteNonQueryAsync("DELETE FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteReaderTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteReaderTests.cs index 6b1b585..b883a32 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteReaderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteReaderTests.cs @@ -1,42 +1,25 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; -public class DbConnectionExtensions_ExecuteReaderTests() : StatementMethodTestsBase( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.ExecuteReaderAsync( - sql, - transaction, - timeout, - CommandBehavior.Default, - commandType, - cancellationToken - ), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.ExecuteReader(sql, transaction, timeout, CommandBehavior.Default, commandType, cancellationToken) -) +public class DbConnectionExtensions_ExecuteReaderTests() + : StatementMethodTestsBase( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.ExecuteReaderAsync( + sql, + transaction, + timeout, + CommandBehavior.Default, + commandType, + cancellationToken + ), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.ExecuteReader(sql, transaction, timeout, CommandBehavior.Default, commandType, cancellationToken) + ) { [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.ExecuteReader("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.ExecuteReader("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.ExecuteReaderAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.ExecuteReaderAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteScalarTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteScalarTests.cs index a041327..0dfb217 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteScalarTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExecuteScalarTests.cs @@ -1,35 +1,18 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; -public class DbConnectionExtensions_ExecuteScalarTests() : StatementMethodTestsBase( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.ExecuteScalarAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.ExecuteScalar(sql, transaction, timeout, commandType, cancellationToken) -) +public class DbConnectionExtensions_ExecuteScalarTests() + : StatementMethodTestsBase( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.ExecuteScalarAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.ExecuteScalar(sql, transaction, timeout, commandType, cancellationToken) + ) { [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.ExecuteScalar("SELECT 1") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.ExecuteScalar("SELECT 1")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.ExecuteScalarAsync("SELECT 1") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.ExecuteScalarAsync("SELECT 1")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExistsTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExistsTests.cs index 56cae06..b0828a1 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExistsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ExistsTests.cs @@ -1,35 +1,18 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; -public class DbConnectionExtensions_ExistsTests() : StatementMethodTestsBase( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.ExistsAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.Exists(sql, transaction, timeout, commandType, cancellationToken) -) +public class DbConnectionExtensions_ExistsTests() + : StatementMethodTestsBase( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.ExistsAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.Exists(sql, transaction, timeout, commandType, cancellationToken) + ) { [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.Exists("SELECT 1") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.Exists("SELECT 1")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.ExistsAsync("SELECT 1") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.ExistsAsync("SELECT 1")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.InsertEntitiesTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.InsertEntitiesTests.cs index 1c4c4d5..c460b1a 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.InsertEntitiesTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.InsertEntitiesTests.cs @@ -3,55 +3,42 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_InsertEntitiesTests : UnitTestsBase { [Fact] - public void InsertEntities_ShouldCallEntityManipulator() + public async Task InsertEntitiesAsync_ShouldCallEntityManipulator() { var entities = Generate.Multiple(); - using var transaction = this.MockDbConnection.BeginTransaction(); + await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.InsertEntities( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.InsertEntitiesAsync(this.MockDbConnection, entities, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - this.MockDbConnection.InsertEntities(entities, transaction, cancellationToken) - .Should().Be(numberOfAffectedRows); + (await this.MockDbConnection.InsertEntitiesAsync(entities, transaction, cancellationToken)) + .Should() + .Be(numberOfAffectedRows); - this.MockEntityManipulator.Received().InsertEntities( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ); + await this + .MockEntityManipulator.Received() + .InsertEntitiesAsync(this.MockDbConnection, entities, transaction, cancellationToken); } [Fact] - public async Task InsertEntitiesAsync_ShouldCallEntityManipulator() + public void InsertEntities_ShouldCallEntityManipulator() { var entities = Generate.Multiple(); - await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); + using var transaction = this.MockDbConnection.BeginTransaction(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.InsertEntitiesAsync( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.InsertEntities(this.MockDbConnection, entities, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - (await this.MockDbConnection.InsertEntitiesAsync(entities, transaction, cancellationToken)) - .Should().Be(numberOfAffectedRows); + this.MockDbConnection.InsertEntities(entities, transaction, cancellationToken) + .Should() + .Be(numberOfAffectedRows); - await this.MockEntityManipulator.Received().InsertEntitiesAsync( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ); + this.MockEntityManipulator.Received() + .InsertEntities(this.MockDbConnection, entities, transaction, cancellationToken); } [Fact] @@ -59,12 +46,8 @@ public void ShouldGuardAgainstNullArguments() { var entities = Generate.Multiple(); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.InsertEntities(entities) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.InsertEntities(entities)); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.InsertEntitiesAsync(entities) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.InsertEntitiesAsync(entities)); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.InsertEntityTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.InsertEntityTests.cs index d1386cb..131e494 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.InsertEntityTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.InsertEntityTests.cs @@ -3,55 +3,40 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_InsertEntityTests : UnitTestsBase { [Fact] - public void InsertEntity_ShouldCallEntityManipulator() + public async Task InsertEntityAsync_ShouldCallEntityManipulator() { var entity = Generate.Single(); - using var transaction = this.MockDbConnection.BeginTransaction(); + await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.InsertEntity( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.InsertEntityAsync(this.MockDbConnection, entity, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - this.MockDbConnection.InsertEntity(entity, transaction, cancellationToken) - .Should().Be(numberOfAffectedRows); + (await this.MockDbConnection.InsertEntityAsync(entity, transaction, cancellationToken)) + .Should() + .Be(numberOfAffectedRows); - this.MockEntityManipulator.Received().InsertEntity( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ); + await this + .MockEntityManipulator.Received() + .InsertEntityAsync(this.MockDbConnection, entity, transaction, cancellationToken); } [Fact] - public async Task InsertEntityAsync_ShouldCallEntityManipulator() + public void InsertEntity_ShouldCallEntityManipulator() { var entity = Generate.Single(); - await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); + using var transaction = this.MockDbConnection.BeginTransaction(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.InsertEntityAsync( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.InsertEntity(this.MockDbConnection, entity, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - (await this.MockDbConnection.InsertEntityAsync(entity, transaction, cancellationToken)) - .Should().Be(numberOfAffectedRows); + this.MockDbConnection.InsertEntity(entity, transaction, cancellationToken).Should().Be(numberOfAffectedRows); - await this.MockEntityManipulator.Received().InsertEntityAsync( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ); + this.MockEntityManipulator.Received() + .InsertEntity(this.MockDbConnection, entity, transaction, cancellationToken); } [Fact] @@ -59,12 +44,8 @@ public void ShouldGuardAgainstNullArguments() { var entity = Generate.Single(); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.InsertEntity(entity) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.InsertEntity(entity)); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.InsertEntityAsync(entity) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.InsertEntityAsync(entity)); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ParameterTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ParameterTests.cs index d01a7d0..8616e96 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ParameterTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.ParameterTests.cs @@ -4,33 +4,29 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_ParameterTests : UnitTestsBase { + private const long TestProductId = 106L; + [Fact] public void Parameter_ShouldInferParameterNameFromValueExpressionIfPossible() { var productId = Generate.Id(); - static Int64 GetProductId() => Generate.Id(); + static long GetProductId() => Generate.Id(); #pragma warning disable RCS1163 // Unused parameter - static Int64 GetProductIdByCategory(String category) => Generate.Id(); + static long GetProductIdByCategory(string category) => Generate.Id(); #pragma warning restore RCS1163 // Unused parameter var productIds = Generate.Ids().ToArray(); - Parameter(productId).InferredName - .Should().Be("ProductId"); + Parameter(productId).InferredName.Should().Be("ProductId"); - Parameter(GetProductId()).InferredName - .Should().Be("ProductId"); + Parameter(GetProductId()).InferredName.Should().Be("ProductId"); - Parameter(GetProductIdByCategory("Shoes")).InferredName - .Should().Be("ProductIdByCategoryShoes"); + Parameter(GetProductIdByCategory("Shoes")).InferredName.Should().Be("ProductIdByCategoryShoes"); - Parameter(productIds[1]).InferredName - .Should().Be("ProductIds1"); + Parameter(productIds[1]).InferredName.Should().Be("ProductIds1"); - Parameter(TestProductId).InferredName - .Should().Be("TestProductId"); + Parameter(TestProductId).InferredName.Should().Be("TestProductId"); - Parameter(new { }).InferredName - .Should().BeNull(); + Parameter(new { }).InferredName.Should().BeNull(); } [Fact] @@ -40,23 +36,20 @@ public void Parameter_ShouldReturnInterpolatedParameter() var interpolatedParameter = Parameter(value); - interpolatedParameter.InferredName - .Should().Be("Value"); + interpolatedParameter.InferredName.Should().Be("Value"); - interpolatedParameter.Value - .Should().Be(value); + interpolatedParameter.Value.Should().Be(value); } [Fact] public void Parameter_ShouldTruncateInferredParameterName() { // ReSharper disable once InconsistentNaming - const Int32 longname_1234567890_1234567890_1234567890_1234567890_1234567890_1234567890 = 1; + const int longname_1234567890_1234567890_1234567890_1234567890_1234567890_1234567890 = 1; - Parameter(longname_1234567890_1234567890_1234567890_1234567890_1234567890_1234567890).InferredName - .Should().HaveLength(60) + Parameter(longname_1234567890_1234567890_1234567890_1234567890_1234567890_1234567890) + .InferredName.Should() + .HaveLength(60) .And.Be("Longname_1234567890_1234567890_1234567890_1234567890_1234567"); } - - private const Int64 TestProductId = 106L; } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOfTTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOfTTests.cs index e28322b..d6379b5 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOfTTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOfTTests.cs @@ -4,38 +4,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QueryFirstOfTTests : StatementMethodTestsBase { - public DbConnectionExtensions_QueryFirstOfTTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryFirstAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryFirst(sql, transaction, timeout, commandType, cancellationToken) - ) + public DbConnectionExtensions_QueryFirstOfTTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QueryFirstAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QueryFirst(sql, transaction, timeout, commandType, cancellationToken) + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); mockDbDataReader.Read().Returns(true); mockDbDataReader.ReadAsync(TestContext.Current.CancellationToken).Returns(true); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); @@ -44,12 +30,8 @@ public DbConnectionExtensions_QueryFirstOfTTests() : base( [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QueryFirst("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QueryFirst("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QueryFirstAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QueryFirstAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs index b04bbc9..04aacdb 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOrDefaultOfTTests.cs @@ -4,38 +4,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QueryFirstOrDefaultOfTTests : StatementMethodTestsBase { - public DbConnectionExtensions_QueryFirstOrDefaultOfTTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryFirstOrDefaultAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryFirstOrDefault(sql, transaction, timeout, commandType, cancellationToken) - ) + public DbConnectionExtensions_QueryFirstOrDefaultOfTTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QueryFirstOrDefaultAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QueryFirstOrDefault(sql, transaction, timeout, commandType, cancellationToken) + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); mockDbDataReader.Read().Returns(true); mockDbDataReader.ReadAsync(TestContext.Current.CancellationToken).Returns(true); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs index 7a6ee39..1f97b0a 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstOrDefaultTests.cs @@ -4,38 +4,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QueryFirstOrDefaultTests : StatementMethodTestsBase { - public DbConnectionExtensions_QueryFirstOrDefaultTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryFirstOrDefaultAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryFirstOrDefault(sql, transaction, timeout, commandType, cancellationToken) - ) + public DbConnectionExtensions_QueryFirstOrDefaultTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QueryFirstOrDefaultAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QueryFirstOrDefault(sql, transaction, timeout, commandType, cancellationToken) + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); mockDbDataReader.Read().Returns(true); mockDbDataReader.ReadAsync(TestContext.Current.CancellationToken).Returns(true); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); @@ -44,12 +30,8 @@ public DbConnectionExtensions_QueryFirstOrDefaultTests() : base( [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QueryFirstOrDefault("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QueryFirstOrDefault("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QueryFirstOrDefaultAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QueryFirstOrDefaultAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstTests.cs index 3090235..3137204 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryFirstTests.cs @@ -4,38 +4,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QueryFirstTests : StatementMethodTestsBase { - public DbConnectionExtensions_QueryFirstTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryFirstAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryFirst(sql, transaction, timeout, commandType, cancellationToken) - ) + public DbConnectionExtensions_QueryFirstTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QueryFirstAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QueryFirst(sql, transaction, timeout, commandType, cancellationToken) + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); mockDbDataReader.Read().Returns(true); mockDbDataReader.ReadAsync(TestContext.Current.CancellationToken).Returns(true); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); @@ -44,12 +30,8 @@ public DbConnectionExtensions_QueryFirstTests() : base( [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QueryFirst("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QueryFirst("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QueryFirstAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QueryFirstAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryOfTTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryOfTTests.cs index 1fc2331..2564366 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryOfTTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryOfTTests.cs @@ -6,36 +6,29 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QueryOfTTests : StatementMethodTestsBase { - public DbConnectionExtensions_QueryOfTTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryAsync(sql, transaction, timeout, commandType, cancellationToken) - .ToListAsync(TestContext.Current.CancellationToken).AsTask(), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.Query(sql, transaction, timeout, commandType, cancellationToken).ToList() - ) + public DbConnectionExtensions_QueryOfTTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection + .QueryAsync(sql, transaction, timeout, commandType, cancellationToken) + .ToListAsync(TestContext.Current.CancellationToken) + .AsTask(), + // Keep this a method call. The lambda is an Action, so its body has to be a STATEMENT, and + // `[.. connection.Query(...)]` is a collection expression - not a valid statement, so it does + // not compile (CS0201). Editors and agents offer that rewrite as a one-click fix; it breaks + // the build. No analyzer suppression is needed - neither `dotnet format style` nor ReSharper's + // cleanup asks for it here, both verified. + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.Query(sql, transaction, timeout, commandType, cancellationToken).ToList() + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); @@ -44,12 +37,8 @@ public DbConnectionExtensions_QueryOfTTests() : base( [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.Query("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.Query("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QueryAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QueryAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOfTTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOfTTests.cs index 365edcd..9569f24 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOfTTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOfTTests.cs @@ -4,38 +4,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QuerySingleOfTTests : StatementMethodTestsBase { - public DbConnectionExtensions_QuerySingleOfTTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QuerySingleAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QuerySingle(sql, transaction, timeout, commandType, cancellationToken) - ) + public DbConnectionExtensions_QuerySingleOfTTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QuerySingleAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QuerySingle(sql, transaction, timeout, commandType, cancellationToken) + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); mockDbDataReader.Read().Returns(true, false); mockDbDataReader.ReadAsync(TestContext.Current.CancellationToken).Returns(true, false); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); @@ -44,12 +30,8 @@ public DbConnectionExtensions_QuerySingleOfTTests() : base( [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QuerySingle("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QuerySingle("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QuerySingleAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QuerySingleAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs index 24544e1..d289a1a 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOrDefaultOfTTests.cs @@ -4,38 +4,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QuerySingleOrDefaultOfTTests : StatementMethodTestsBase { - public DbConnectionExtensions_QuerySingleOrDefaultOfTTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QuerySingleOrDefaultAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QuerySingleOrDefault(sql, transaction, timeout, commandType, cancellationToken) - ) + public DbConnectionExtensions_QuerySingleOrDefaultOfTTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QuerySingleOrDefaultAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QuerySingleOrDefault(sql, transaction, timeout, commandType, cancellationToken) + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); mockDbDataReader.Read().Returns(true, false); mockDbDataReader.ReadAsync(TestContext.Current.CancellationToken).Returns(true, false); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs index afa4d0b..d80d198 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleOrDefaultTests.cs @@ -4,38 +4,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QuerySingleOrDefaultTests : StatementMethodTestsBase { - public DbConnectionExtensions_QuerySingleOrDefaultTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QuerySingleOrDefaultAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QuerySingleOrDefault(sql, transaction, timeout, commandType, cancellationToken) - ) + public DbConnectionExtensions_QuerySingleOrDefaultTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QuerySingleOrDefaultAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QuerySingleOrDefault(sql, transaction, timeout, commandType, cancellationToken) + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); mockDbDataReader.Read().Returns(true, false); mockDbDataReader.ReadAsync(TestContext.Current.CancellationToken).Returns(true, false); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); @@ -44,12 +30,8 @@ public DbConnectionExtensions_QuerySingleOrDefaultTests() : base( [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QuerySingleOrDefault("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QuerySingleOrDefault("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QuerySingleOrDefaultAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QuerySingleOrDefaultAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleTests.cs index 5496899..b3d846e 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QuerySingleTests.cs @@ -4,38 +4,24 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QuerySingleTests : StatementMethodTestsBase { - public DbConnectionExtensions_QuerySingleTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QuerySingleAsync(sql, transaction, timeout, commandType, cancellationToken), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QuerySingle(sql, transaction, timeout, commandType, cancellationToken) - ) + public DbConnectionExtensions_QuerySingleTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QuerySingleAsync(sql, transaction, timeout, commandType, cancellationToken), + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.QuerySingle(sql, transaction, timeout, commandType, cancellationToken) + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); mockDbDataReader.Read().Returns(true, false); mockDbDataReader.ReadAsync(TestContext.Current.CancellationToken).Returns(true, false); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); @@ -44,12 +30,8 @@ public DbConnectionExtensions_QuerySingleTests() : base( [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QuerySingle("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QuerySingle("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QuerySingleAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QuerySingleAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryTests.cs index 0bccc94..786eff5 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.QueryTests.cs @@ -6,37 +6,29 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_QueryTests : StatementMethodTestsBase { - public DbConnectionExtensions_QueryTests() : base( - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.QueryAsync(sql, transaction, timeout, commandType, cancellationToken) - .ToListAsync(cancellationToken) - .AsTask(), - ( - connection, - sql, - transaction, - timeout, - commandType, - cancellationToken - ) => - connection.Query(sql, transaction, timeout, commandType, cancellationToken).ToList() - ) + public DbConnectionExtensions_QueryTests() + : base( + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection + .QueryAsync(sql, transaction, timeout, commandType, cancellationToken) + .ToListAsync(cancellationToken) + .AsTask(), + // Keep this a method call. The lambda is an Action, so its body has to be a STATEMENT, and + // `[.. connection.Query(...)]` is a collection expression - not a valid statement, so it does + // not compile (CS0201). Editors and agents offer that rewrite as a one-click fix; it breaks + // the build. No analyzer suppression is needed - neither `dotnet format style` nor ReSharper's + // cleanup asks for it here, both verified. + (connection, sql, transaction, timeout, commandType, cancellationToken) => + connection.Query(sql, transaction, timeout, commandType, cancellationToken).ToList() + ) { var mockDbDataReader = Substitute.For(); mockDbDataReader.FieldCount.Returns(1); mockDbDataReader.GetName(0).Returns("Id"); - mockDbDataReader.GetFieldType(0).Returns(typeof(Int64)); + mockDbDataReader.GetFieldType(0).Returns(typeof(long)); - this.MockDbCommand.ExecuteReader(Arg.Any()) - .Returns(mockDbDataReader); + this.MockDbCommand.ExecuteReader(Arg.Any()).Returns(mockDbDataReader); this.MockDbCommand.ExecuteReaderAsync(Arg.Any(), Arg.Any()) .Returns(mockDbDataReader); @@ -45,12 +37,8 @@ public DbConnectionExtensions_QueryTests() : base( [Fact] public void ShouldGuardAgainstNullArguments() { - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.Query("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.Query("SELECT * FROM Entity")); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.QueryAsync("SELECT * FROM Entity") - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.QueryAsync("SELECT * FROM Entity")); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.TemporaryTableTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.TemporaryTableTests.cs index dae1d01..de6df49 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.TemporaryTableTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.TemporaryTableTests.cs @@ -4,34 +4,31 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; public class DbConnectionExtensions_TemporaryTableTests : UnitTestsBase { + private readonly List testEntityIds = Generate.Ids(); + [Fact] public void ShouldGuardAgainstNullArguments() => - ArgumentNullGuardVerifier.Verify(() => TemporaryTable(new List())); + ArgumentNullGuardVerifier.Verify(() => TemporaryTable(new List())); [Fact] public void TemporaryTable_ShouldInferTableNameFromValuesExpressionIfPossible() { var entityIds = Generate.Ids(); - static List Get() => Generate.Ids(); - static List GetEntityIds() => Generate.Ids(); + static List Get() => Generate.Ids(); + static List GetEntityIds() => Generate.Ids(); #pragma warning disable RCS1163 // Unused parameter - static List GetEntityIdsByCategory(String category) => Generate.Ids(); + static List GetEntityIdsByCategory(string category) => Generate.Ids(); #pragma warning restore RCS1163 // Unused parameter - TemporaryTable(entityIds).Name - .Should().StartWith("EntityIds_"); + TemporaryTable(entityIds).Name.Should().StartWith("EntityIds_"); - TemporaryTable(GetEntityIds()).Name - .Should().StartWith("EntityIds_"); + TemporaryTable(GetEntityIds()).Name.Should().StartWith("EntityIds_"); - TemporaryTable(GetEntityIdsByCategory("Shoes")).Name - .Should().StartWith("EntityIdsByCategoryShoes_"); + TemporaryTable(GetEntityIdsByCategory("Shoes")).Name.Should().StartWith("EntityIdsByCategoryShoes_"); - TemporaryTable(this.testEntityIds).Name - .Should().StartWith("TestEntityIds_"); + TemporaryTable(this.testEntityIds).Name.Should().StartWith("TestEntityIds_"); - TemporaryTable(Get()).Name - .Should().StartWith("Values_"); + TemporaryTable(Get()).Name.Should().StartWith("Values_"); } [Fact] @@ -41,47 +38,39 @@ public void TemporaryTable_ShouldReturnInterpolatedTemporaryTable() var temporaryTable1 = TemporaryTable(entityIds); - temporaryTable1.Values - .Should().BeSameAs(entityIds); + temporaryTable1.Values.Should().BeSameAs(entityIds); - temporaryTable1.ValuesType - .Should().Be(typeof(Int64)); + temporaryTable1.ValuesType.Should().Be(typeof(long)); - temporaryTable1.Name - .Should().StartWith("EntityIds_"); + temporaryTable1.Name.Should().StartWith("EntityIds_"); var entities = Generate.Multiple(); var temporaryTable2 = TemporaryTable(entities); - temporaryTable2.Values - .Should().BeSameAs(entities); + temporaryTable2.Values.Should().BeSameAs(entities); - temporaryTable2.ValuesType - .Should().Be(typeof(Entity)); + temporaryTable2.ValuesType.Should().Be(typeof(Entity)); - temporaryTable2.Name - .Should().StartWith("Entities_"); + temporaryTable2.Name.Should().StartWith("Entities_"); } [Fact] public void TemporaryTable_ShouldTruncateInferredTableName() { // ReSharper disable once InconsistentNaming - Int32[] longname_1234567890_1234567890_1234567890_1234567890_1234567890_1234567890 = [1, 2, 3]; + int[] longname_1234567890_1234567890_1234567890_1234567890_1234567890_1234567890 = [1, 2, 3]; - TemporaryTable(longname_1234567890_1234567890_1234567890_1234567890_1234567890_1234567890).Name - .Should().HaveLength(60) + TemporaryTable(longname_1234567890_1234567890_1234567890_1234567890_1234567890_1234567890) + .Name.Should() + .HaveLength(60) .And.StartWith("Longname_1234567890_1234567_"); } [Fact] public void TemporaryTable_TIsObject_ShouldThrow() => - Invoking(() => TemporaryTable(new List())) - .Should().Throw() - .WithMessage( - $"The type parameter T cannot be the type {typeof(Object)}." - ); - - private readonly List testEntityIds = Generate.Ids(); + Invoking(() => TemporaryTable(new List())) + .Should() + .Throw() + .WithMessage($"The type parameter T cannot be the type {typeof(object)}."); } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntitiesTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntitiesTests.cs index f6b4691..deae9d6 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntitiesTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntitiesTests.cs @@ -7,64 +7,47 @@ public void ShouldGuardAgainstNullArguments() { var entities = Generate.Multiple(); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.UpdateEntities(entities) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.UpdateEntities(entities)); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.UpdateEntitiesAsync(entities) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.UpdateEntitiesAsync(entities)); } [Fact] - public void UpdateEntities_ShouldCallEntityManipulator() + public async Task UpdateEntitiesAsync_ShouldCallEntityManipulator() { var entities = Generate.Multiple(); - using var transaction = this.MockDbConnection.BeginTransaction(); + await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.UpdateEntities( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.UpdateEntitiesAsync(this.MockDbConnection, entities, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - this.MockDbConnection.UpdateEntities(entities, transaction, cancellationToken) - .Should().Be(numberOfAffectedRows); + (await this.MockDbConnection.UpdateEntitiesAsync(entities, transaction, cancellationToken)) + .Should() + .Be(numberOfAffectedRows); - this.MockEntityManipulator.Received().UpdateEntities( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ); + await this + .MockEntityManipulator.Received() + .UpdateEntitiesAsync(this.MockDbConnection, entities, transaction, cancellationToken); } [Fact] - public async Task UpdateEntitiesAsync_ShouldCallEntityManipulator() + public void UpdateEntities_ShouldCallEntityManipulator() { var entities = Generate.Multiple(); - await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); + using var transaction = this.MockDbConnection.BeginTransaction(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.UpdateEntitiesAsync( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.UpdateEntities(this.MockDbConnection, entities, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - (await this.MockDbConnection.UpdateEntitiesAsync(entities, transaction, cancellationToken)) - .Should().Be(numberOfAffectedRows); + this.MockDbConnection.UpdateEntities(entities, transaction, cancellationToken) + .Should() + .Be(numberOfAffectedRows); - await this.MockEntityManipulator.Received().UpdateEntitiesAsync( - this.MockDbConnection, - entities, - transaction, - cancellationToken - ); + this.MockEntityManipulator.Received() + .UpdateEntities(this.MockDbConnection, entities, transaction, cancellationToken); } } diff --git a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntityTests.cs b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntityTests.cs index fdb004b..b58dc9b 100644 --- a/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntityTests.cs +++ b/tests/DbConnectionPlus.UnitTests/DbConnectionExtensions.UpdateEntityTests.cs @@ -7,64 +7,45 @@ public void ShouldGuardAgainstNullArguments() { var entity = Generate.Single(); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.UpdateEntity(entity) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.UpdateEntity(entity)); - ArgumentNullGuardVerifier.Verify(() => - this.MockDbConnection.UpdateEntityAsync(entity) - ); + ArgumentNullGuardVerifier.Verify(() => this.MockDbConnection.UpdateEntityAsync(entity)); } [Fact] - public void UpdateEntity_ShouldCallEntityManipulator() + public async Task UpdateEntityAsync_ShouldCallEntityManipulator() { var entity = Generate.Single(); - using var transaction = this.MockDbConnection.BeginTransaction(); + await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.UpdateEntity( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.UpdateEntityAsync(this.MockDbConnection, entity, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - this.MockDbConnection.UpdateEntity(entity, transaction, cancellationToken) - .Should().Be(numberOfAffectedRows); + (await this.MockDbConnection.UpdateEntityAsync(entity, transaction, cancellationToken)) + .Should() + .Be(numberOfAffectedRows); - this.MockEntityManipulator.Received().UpdateEntity( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ); + await this + .MockEntityManipulator.Received() + .UpdateEntityAsync(this.MockDbConnection, entity, transaction, cancellationToken); } [Fact] - public async Task UpdateEntityAsync_ShouldCallEntityManipulator() + public void UpdateEntity_ShouldCallEntityManipulator() { var entity = Generate.Single(); - await using var transaction = await this.MockDbConnection.BeginTransactionAsync(); + using var transaction = this.MockDbConnection.BeginTransaction(); var cancellationToken = TestContext.Current.CancellationToken; var numberOfAffectedRows = Generate.SmallNumber(); - this.MockEntityManipulator.UpdateEntityAsync( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ).Returns(numberOfAffectedRows); + this.MockEntityManipulator.UpdateEntity(this.MockDbConnection, entity, transaction, cancellationToken) + .Returns(numberOfAffectedRows); - (await this.MockDbConnection.UpdateEntityAsync(entity, transaction, cancellationToken)) - .Should().Be(numberOfAffectedRows); + this.MockDbConnection.UpdateEntity(entity, transaction, cancellationToken).Should().Be(numberOfAffectedRows); - await this.MockEntityManipulator.Received().UpdateEntityAsync( - this.MockDbConnection, - entity, - transaction, - cancellationToken - ); + this.MockEntityManipulator.Received() + .UpdateEntity(this.MockDbConnection, entity, transaction, cancellationToken); } } diff --git a/tests/DbConnectionPlus.UnitTests/Dynamic/DataRowTests.cs b/tests/DbConnectionPlus.UnitTests/Dynamic/DataRowTests.cs index 5455245..f99fcca 100644 --- a/tests/DbConnectionPlus.UnitTests/Dynamic/DataRowTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Dynamic/DataRowTests.cs @@ -9,71 +9,26 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Dynamic; public class DataRowTests : UnitTestsBase { - [Fact] - public void ShouldBeMutable() - { - var dictionary = new Dictionary - { - { "ColumnA", Generate.ScalarValue() }, - { "ColumnB", Generate.ScalarValue() }, - { "ColumnC", Generate.ScalarValue() } - }; - - var dataRow = new DataRow(dictionary); - - dataRow["ColumnA"] - .Should().Be(dictionary["ColumnA"]); - - dataRow["ColumnB"] - .Should().Be(dictionary["ColumnB"]); - - dataRow["ColumnC"] - .Should().Be(dictionary["ColumnC"]); - - var newValueA = Generate.ScalarValue(); - dataRow["ColumnA"] = newValueA; - - dataRow["ColumnA"] - .Should().Be(newValueA); - - var newValueB = Generate.ScalarValue(); - dataRow["ColumnB"] = newValueB; - - dataRow["ColumnB"] - .Should().Be(newValueB); - - var newValueC = Generate.ScalarValue(); - dataRow["ColumnC"] = newValueC; - - dataRow["ColumnC"] - .Should().Be(newValueC); - } - [Fact] public void ShouldAllowDynamicMemberAccess() { - var dictionary = new Dictionary + var dictionary = new Dictionary { { "ColumnA", Generate.ScalarValue() }, - { "ColumnB", Generate.ScalarValue() } + { "ColumnB", Generate.ScalarValue() }, }; dynamic dataRow = new DataRow(dictionary); - ((Object?)dataRow.ColumnA) - .Should().Be(dictionary["ColumnA"]); + ((object?)dataRow.ColumnA).Should().Be(dictionary["ColumnA"]); - ((Object?)dataRow.ColumnB) - .Should().Be(dictionary["ColumnB"]); + ((object?)dataRow.ColumnB).Should().Be(dictionary["ColumnB"]); } [Fact] public void ShouldAllowDynamicMemberAssignment() { - var dictionary = new Dictionary - { - { "ColumnA", Generate.ScalarValue() } - }; + var dictionary = new Dictionary { { "ColumnA", Generate.ScalarValue() } }; var dataRow = new DataRow(dictionary); dynamic dynamicDataRow = dataRow; @@ -81,147 +36,160 @@ public void ShouldAllowDynamicMemberAssignment() var newValue = Generate.ScalarValue(); dynamicDataRow.ColumnA = newValue; - dataRow["ColumnA"] - .Should().Be(newValue); + dataRow["ColumnA"].Should().Be(newValue); - dictionary["ColumnA"] - .Should().Be(newValue); + dictionary["ColumnA"].Should().Be(newValue); } [Fact] public void ShouldAllowDynamicMemberAssignmentOfUnknownColumn() { - var dataRow = new DataRow(new Dictionary()); + var dataRow = new DataRow(new Dictionary()); dynamic dynamicDataRow = dataRow; var value = Generate.ScalarValue(); dynamicDataRow.NewColumn = value; - dataRow["NewColumn"] - .Should().Be(value); + dataRow["NewColumn"].Should().Be(value); } [Fact] - public void ShouldProvideDynamicMemberNames() + public void ShouldBeMutable() { - var dictionary = new Dictionary + var dictionary = new Dictionary { { "ColumnA", Generate.ScalarValue() }, - { "ColumnB", Generate.ScalarValue() } + { "ColumnB", Generate.ScalarValue() }, + { "ColumnC", Generate.ScalarValue() }, }; - IDynamicMetaObjectProvider dataRow = new DataRow(dictionary); + var dataRow = new DataRow(dictionary); - var metaObject = dataRow.GetMetaObject(Expression.Constant(dataRow)); + dataRow["ColumnA"].Should().Be(dictionary["ColumnA"]); - metaObject.GetDynamicMemberNames() - .Should().BeEquivalentTo("ColumnA", "ColumnB"); - } + dataRow["ColumnB"].Should().Be(dictionary["ColumnB"]); - [Fact] - public void ShouldThrowWhenDynamicallyReadingUnknownColumn() - { - dynamic dataRow = new DataRow(new Dictionary()); + dataRow["ColumnC"].Should().Be(dictionary["ColumnC"]); - Invoking(() => (Object?)dataRow.UnknownColumn) - .Should().Throw(); - } + var newValueA = Generate.ScalarValue(); + dataRow["ColumnA"] = newValueA; - [Fact] - public void ShouldResolveDynamicPropertyAccessToColumnsAndNotToOwnProperties() - { - dynamic dataRow = new DataRow(new Dictionary { { "ColumnA", Generate.ScalarValue() } }); + dataRow["ColumnA"].Should().Be(newValueA); - // "Count" is a property of DataRow, but through a dynamic reference it addresses a column of that name. - Invoking(() => (Object?)dataRow.Count) - .Should().Throw(); + var newValueB = Generate.ScalarValue(); + dataRow["ColumnB"] = newValueB; - dynamic rowWithShadowingColumn = new DataRow(new Dictionary { { "Count", 42 } }); + dataRow["ColumnB"].Should().Be(newValueB); + + var newValueC = Generate.ScalarValue(); + dataRow["ColumnC"] = newValueC; - ((Object?)rowWithShadowingColumn.Count) - .Should().Be(42); + dataRow["ColumnC"].Should().Be(newValueC); } [Fact] - public void ShouldResolveDynamicMethodCallsToOwnMembers() + public void ShouldForwardAllMethodCallsToDictionary() { - dynamic dataRow = new DataRow(new Dictionary { { "ColumnA", Generate.ScalarValue() } }); + var exceptions = new HashSet { nameof(IDictionary<,>.TryGetValue) }; - ((Boolean)dataRow.ContainsKey("ColumnA")) - .Should().BeTrue(); + var fixture = new Fixture(); + fixture.Customize(new AutoNSubstituteCustomization()); + fixture.Register(() => new DataTable()); - ((Boolean)dataRow.ContainsKey("ColumnB")) - .Should().BeFalse(); + var dictionary = Substitute.For>(); + var dataRow = new DataRow(dictionary); + + DecoratorAssertions.AssertDecoratorForwardsAllCalls(fixture, dataRow, dictionary, exceptions); } [Fact] - public void ShouldForwardAllMethodCallsToDictionary() + public void ShouldProvideDynamicMemberNames() { - var exceptions = new HashSet + var dictionary = new Dictionary { - nameof(IDictionary<,>.TryGetValue) + { "ColumnA", Generate.ScalarValue() }, + { "ColumnB", Generate.ScalarValue() }, }; - var fixture = new Fixture(); - fixture.Customize(new AutoNSubstituteCustomization()); - fixture.Register(() => new DataTable()); + IDynamicMetaObjectProvider dataRow = new DataRow(dictionary); - var dictionary = Substitute.For>(); - var dataRow = new DataRow(dictionary); + var metaObject = dataRow.GetMetaObject(Expression.Constant(dataRow)); - DecoratorAssertions.AssertDecoratorForwardsAllCalls( - fixture, - dataRow, - dictionary, - exceptions - ); + metaObject.GetDynamicMemberNames().Should().BeEquivalentTo("ColumnA", "ColumnB"); } [Fact] public void ShouldProvideRowData() { - var dictionary = new Dictionary + var dictionary = new Dictionary { { "ColumnA", Generate.ScalarValue() }, { "ColumnB", Generate.ScalarValue() }, - { "ColumnC", Generate.ScalarValue() } + { "ColumnC", Generate.ScalarValue() }, }; var dataRow = new DataRow(dictionary); - dataRow["ColumnA"] - .Should().Be(dictionary["ColumnA"]); + dataRow["ColumnA"].Should().Be(dictionary["ColumnA"]); - dataRow["ColumnB"] - .Should().Be(dictionary["ColumnB"]); + dataRow["ColumnB"].Should().Be(dictionary["ColumnB"]); + + dataRow["ColumnC"].Should().Be(dictionary["ColumnC"]); + } + + [Fact] + public void ShouldResolveDynamicMethodCallsToOwnMembers() + { + dynamic dataRow = new DataRow(new Dictionary { { "ColumnA", Generate.ScalarValue() } }); + + ((bool)dataRow.ContainsKey("ColumnA")).Should().BeTrue(); + + ((bool)dataRow.ContainsKey("ColumnB")).Should().BeFalse(); + } + + [Fact] + public void ShouldResolveDynamicPropertyAccessToColumnsAndNotToOwnProperties() + { + dynamic dataRow = new DataRow(new Dictionary { { "ColumnA", Generate.ScalarValue() } }); + + // "Count" is a property of DataRow, but through a dynamic reference it addresses a column of that name. + Invoking(() => (object?)dataRow.Count).Should().Throw(); + + dynamic rowWithShadowingColumn = new DataRow(new Dictionary { { "Count", 42 } }); + + ((object?)rowWithShadowingColumn.Count).Should().Be(42); + } + + [Fact] + public void ShouldThrowWhenDynamicallyReadingUnknownColumn() + { + dynamic dataRow = new DataRow(new Dictionary()); - dataRow["ColumnC"] - .Should().Be(dictionary["ColumnC"]); + Invoking(() => (object?)dataRow.UnknownColumn).Should().Throw(); } [Fact] public void TryGetValue_ShouldForwardCallToDictionary() { - var key = Generate.Single(); + var key = Generate.Single(); var value = Generate.ScalarValue(); - var dictionary = Substitute.For>(); + var dictionary = Substitute.For>(); - dictionary.TryGetValue(key, out Arg.Any()).Returns(a => + dictionary + .TryGetValue(key, out Arg.Any()) + .Returns(a => { a[1] = value; return true; - } - ); + }); var dataRow = new DataRow(dictionary); - dataRow.TryGetValue(key, out var result) - .Should().BeTrue(); + dataRow.TryGetValue(key, out var result).Should().BeTrue(); - result - .Should().Be(value); + result.Should().Be(value); - dictionary.Received().TryGetValue(key, out Arg.Any()); + dictionary.Received().TryGetValue(key, out Arg.Any()); } } diff --git a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs index 325aed5..55e1481 100644 --- a/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Entities/EntityHelperTests.cs @@ -8,21 +8,30 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Entities; public class EntityHelperTests : UnitTestsBase { + /// + /// The method. + /// + private static readonly MethodInfo specimenFactoryCreateMethod = typeof(SpecimenFactory).GetMethod( + nameof(SpecimenFactory.Create), + BindingFlags.Public | BindingFlags.Static, + [typeof(ISpecimenBuilder)] + )!; + [Fact] public void FindCompatibleConstructor_MatchingPrivateConstructor_ShouldReturnPrivateConstructor() { var constructor = EntityHelper.FindCompatibleConstructor( typeof(ItemWithPrivateConstructor), - [("c", typeof(Int64)), ("b", typeof(Int32)), ("a", typeof(Int16))] + [("c", typeof(long)), ("b", typeof(int)), ("a", typeof(short))] ); - constructor - .Should().NotBeNull(); + constructor.Should().NotBeNull(); constructor .GetParameters() .Select(a => (a.Name, a.ParameterType)) - .Should().BeEquivalentTo([("a", typeof(Int16)), ("b", typeof(Int32)), ("c", typeof(Int64))]); + .Should() + .BeEquivalentTo([("a", typeof(short)), ("b", typeof(int)), ("c", typeof(long))]); } [Fact] @@ -30,100 +39,105 @@ public void FindCompatibleConstructor_NamesAndTypesMatchWithDifferentOrder_Shoul { var constructor = EntityHelper.FindCompatibleConstructor( typeof(ItemWithConstructor), - [("c", typeof(Int64)), ("b", typeof(Int32)), ("a", typeof(Int16))] + [("c", typeof(long)), ("b", typeof(int)), ("a", typeof(short))] ); - constructor - .Should().NotBeNull(); + constructor.Should().NotBeNull(); constructor .GetParameters() .Select(a => (a.Name, a.ParameterType)) - .Should().BeEquivalentTo([("a", typeof(Int16)), ("b", typeof(Int32)), ("c", typeof(Int64))]); + .Should() + .BeEquivalentTo([("a", typeof(short)), ("b", typeof(int)), ("c", typeof(long))]); } [Fact] public void FindCompatibleConstructor_NamesDoNotMatch_TypesMatch_ShouldReturnNull() => - EntityHelper.FindCompatibleConstructor( + EntityHelper + .FindCompatibleConstructor( typeof(ItemWithConstructor), - [("d", typeof(Int16)), ("e", typeof(Int32)), ("f", typeof(Int64))] + [("d", typeof(short)), ("e", typeof(int)), ("f", typeof(long))] ) - .Should().BeNull(); + .Should() + .BeNull(); [Fact] - public void FindCompatibleConstructor_NamesMatch_TypesAreCompatible_ShouldReturnConstructor() + public void FindCompatibleConstructor_NamesMatchWithDifferentCasing_TypesMatch_ShouldReturnConstructor() { var constructor = EntityHelper.FindCompatibleConstructor( typeof(ItemWithConstructor), - [("a", typeof(Int32)), ("b", typeof(Int32)), ("c", typeof(Int32))] + [("A", typeof(short)), ("B", typeof(int)), ("C", typeof(long))] ); - constructor - .Should().NotBeNull(); + constructor.Should().NotBeNull(); constructor .GetParameters() - .Select(a => (a.Name, a.ParameterType)) - .Should().BeEquivalentTo([("a", typeof(Int16)), ("b", typeof(Int32)), ("c", typeof(Int64))]); + .Select(a => a.ParameterType) + .Should() + .BeEquivalentTo([typeof(short), typeof(int), typeof(long)]); } [Fact] - public void FindCompatibleConstructor_NamesMatch_TypesAreIncompatible_ShouldReturnNull() => - EntityHelper.FindCompatibleConstructor( - typeof(ItemWithConstructor), - [("a", typeof(Int16)), ("b", typeof(Int32)), ("c", typeof(TimeSpan))] - ) - .Should().BeNull(); - - [Fact] - public void FindCompatibleConstructor_NamesMatch_TypesMatch_ShouldReturnConstructor() + public void FindCompatibleConstructor_NamesMatch_TypesAreCompatible_ShouldReturnConstructor() { var constructor = EntityHelper.FindCompatibleConstructor( typeof(ItemWithConstructor), - [("a", typeof(Int16)), ("b", typeof(Int32)), ("c", typeof(Int64))] + [("a", typeof(int)), ("b", typeof(int)), ("c", typeof(int))] ); - constructor - .Should().NotBeNull(); + constructor.Should().NotBeNull(); constructor .GetParameters() .Select(a => (a.Name, a.ParameterType)) - .Should().BeEquivalentTo([("a", typeof(Int16)), ("b", typeof(Int32)), ("c", typeof(Int64))]); + .Should() + .BeEquivalentTo([("a", typeof(short)), ("b", typeof(int)), ("c", typeof(long))]); } [Fact] - public void FindCompatibleConstructor_NamesMatchWithDifferentCasing_TypesMatch_ShouldReturnConstructor() + public void FindCompatibleConstructor_NamesMatch_TypesAreIncompatible_ShouldReturnNull() => + EntityHelper + .FindCompatibleConstructor( + typeof(ItemWithConstructor), + [("a", typeof(short)), ("b", typeof(int)), ("c", typeof(TimeSpan))] + ) + .Should() + .BeNull(); + + [Fact] + public void FindCompatibleConstructor_NamesMatch_TypesMatch_ShouldReturnConstructor() { var constructor = EntityHelper.FindCompatibleConstructor( typeof(ItemWithConstructor), - [("A", typeof(Int16)), ("B", typeof(Int32)), ("C", typeof(Int64))] + [("a", typeof(short)), ("b", typeof(int)), ("c", typeof(long))] ); - constructor - .Should().NotBeNull(); + constructor.Should().NotBeNull(); constructor .GetParameters() - .Select(a => a.ParameterType) - .Should().BeEquivalentTo([typeof(Int16), typeof(Int32), typeof(Int64)]); + .Select(a => (a.Name, a.ParameterType)) + .Should() + .BeEquivalentTo([("a", typeof(short)), ("b", typeof(int)), ("c", typeof(long))]); } [Fact] public void FindCompatibleConstructor_NoMatchingConstructor_ShouldReturnNull() => - EntityHelper.FindCompatibleConstructor( + EntityHelper + .FindCompatibleConstructor( typeof(ItemWithConstructor), - [("a", typeof(Int16)), ("b", typeof(Int32)), ("c", typeof(Int64)), ("d", typeof(String))] + [("a", typeof(short)), ("b", typeof(int)), ("c", typeof(long)), ("d", typeof(string))] ) - .Should().BeNull(); + .Should() + .BeNull(); [Fact] public void FindParameterlessConstructor_NoParameterlessConstructor_ShouldReturnNull() { var constructor = EntityHelper.FindParameterlessConstructor(typeof(EntityWithPublicConstructor)); - constructor - .Should().BeNull(); + constructor.Should().BeNull(); } [Fact] @@ -132,7 +146,8 @@ public void FindParameterlessConstructor_PrivateParameterlessConstructor_ShouldR var constructor = EntityHelper.FindParameterlessConstructor(typeof(ItemWithPrivateParameterlessConstructor)); constructor - .Should().BeSameAs( + .Should() + .BeSameAs( typeof(ItemWithPrivateParameterlessConstructor).GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, Type.EmptyTypes @@ -146,9 +161,8 @@ public void FindParameterlessConstructor_PublicParameterlessConstructor_ShouldRe var constructor = EntityHelper.FindParameterlessConstructor(typeof(Entity)); constructor - .Should().BeSameAs( - typeof(Entity).GetConstructor(BindingFlags.Public | BindingFlags.Instance, Type.EmptyTypes) - ); + .Should() + .BeSameAs(typeof(Entity).GetConstructor(BindingFlags.Public | BindingFlags.Instance, Type.EmptyTypes)); } [Fact] @@ -162,79 +176,75 @@ public void GetEntityTypeMetadata_Mapping_Attributes_ShouldGetMetadataBasedOnAtt var entityType = typeof(MappingTestEntityAttributes); - var entity = specimenFactoryCreateMethod - .MakeGenericMethod(entityType) - .Invoke(null, [fixture])!; + var entity = specimenFactoryCreateMethod.MakeGenericMethod(entityType).Invoke(null, [fixture])!; var entityProperties = entityType.GetProperties(BindingFlags.Public | BindingFlags.Instance); var metadata = EntityHelper.GetEntityTypeMetadata(entityType); - metadata - .Should().NotBeNull(); + metadata.Should().NotBeNull(); - metadata.EntityType - .Should().Be(entityType); + metadata.EntityType.Should().Be(entityType); - metadata.TableName - .Should().Be(entityType.GetCustomAttribute()?.Name ?? entityType.Name); + metadata.TableName.Should().Be(entityType.GetCustomAttribute()?.Name ?? entityType.Name); var allPropertiesMetadata = metadata.AllProperties; - allPropertiesMetadata - .Should().HaveSameCount(entityProperties); + allPropertiesMetadata.Should().HaveSameCount(entityProperties); - metadata.AllPropertiesByPropertyName - .Should().BeEquivalentTo(allPropertiesMetadata.ToDictionary(a => a.PropertyName)); + metadata + .AllPropertiesByPropertyName.Should() + .BeEquivalentTo(allPropertiesMetadata.ToDictionary(a => a.PropertyName)); - metadata.ComputedProperties - .Should() + metadata + .ComputedProperties.Should() .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsComputed: true })); - metadata.ConcurrencyTokenProperties - .Should() + metadata + .ConcurrencyTokenProperties.Should() .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsConcurrencyToken: true })); - metadata.DatabaseGeneratedProperties - .Should() + metadata + .DatabaseGeneratedProperties.Should() .BeEquivalentTo( allPropertiesMetadata.Where(a => !a.IsIgnored && (a.IsComputed || a.IsIdentity || a.IsRowVersion)) ); - metadata.IdentityProperty - .Should() + metadata + .IdentityProperty.Should() .Be(allPropertiesMetadata.FirstOrDefault(a => a is { IsIgnored: false, IsIdentity: true })); - metadata.InsertProperties - .Should().BeEquivalentTo( - allPropertiesMetadata.Where(a => a is - { IsIgnored: false, IsComputed: false, IsIdentity: false, IsRowVersion: false } + metadata + .InsertProperties.Should() + .BeEquivalentTo( + allPropertiesMetadata.Where(a => + a is { IsIgnored: false, IsComputed: false, IsIdentity: false, IsRowVersion: false } ) ); - metadata.KeyProperties - .Should() + metadata + .KeyProperties.Should() .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false, IsKey: true })); - metadata.MappedProperties - .Should() - .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false })); + metadata.MappedProperties.Should().BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsIgnored: false })); - metadata.RowVersionProperties - .Should() + metadata + .RowVersionProperties.Should() .BeEquivalentTo(allPropertiesMetadata.Where(a => a is { IsRowVersion: true })); - metadata.UpdateProperties - .Should().BeEquivalentTo( - allPropertiesMetadata.Where(a => a is - { - IsComputed: false, - IsConcurrencyToken: false, - IsIgnored: false, - IsIdentity: false, - IsKey: false, - IsRowVersion: false - } + metadata + .UpdateProperties.Should() + .BeEquivalentTo( + allPropertiesMetadata.Where(a => + a + is { + IsComputed: false, + IsConcurrencyToken: false, + IsIgnored: false, + IsIdentity: false, + IsKey: false, + IsRowVersion: false + } ) ); @@ -242,66 +252,58 @@ public void GetEntityTypeMetadata_Mapping_Attributes_ShouldGetMetadataBasedOnAtt { var propertyMetadata = allPropertiesMetadata.FirstOrDefault(a => a.PropertyName == property.Name); - propertyMetadata - .Should().NotBeNull(); + propertyMetadata.Should().NotBeNull(); - propertyMetadata.CanRead - .Should().Be(property.CanRead); + propertyMetadata.CanRead.Should().Be(property.CanRead); - propertyMetadata.CanWrite - .Should().Be(property.CanWrite); + propertyMetadata.CanWrite.Should().Be(property.CanWrite); - propertyMetadata.ColumnName - .Should().Be(property.GetCustomAttribute()?.Name ?? property.Name); + propertyMetadata + .ColumnName.Should() + .Be(property.GetCustomAttribute()?.Name ?? property.Name); - propertyMetadata.IsComputed - .Should().Be( - property.GetCustomAttribute()?.DatabaseGeneratedOption is - DatabaseGeneratedOption.Computed + propertyMetadata + .IsComputed.Should() + .Be( + property.GetCustomAttribute()?.DatabaseGeneratedOption + is DatabaseGeneratedOption.Computed ); - propertyMetadata.IsConcurrencyToken - .Should().Be(property.GetCustomAttribute() is not null); + propertyMetadata + .IsConcurrencyToken.Should() + .Be(property.GetCustomAttribute() is not null); - propertyMetadata.IsIdentity - .Should().Be( - property.GetCustomAttribute()?.DatabaseGeneratedOption is - DatabaseGeneratedOption.Identity + propertyMetadata + .IsIdentity.Should() + .Be( + property.GetCustomAttribute()?.DatabaseGeneratedOption + is DatabaseGeneratedOption.Identity ); - propertyMetadata.IsIgnored - .Should().Be(property.GetCustomAttribute() is not null); + propertyMetadata.IsIgnored.Should().Be(property.GetCustomAttribute() is not null); - propertyMetadata.IsKey - .Should().Be(property.GetCustomAttribute() is not null); + propertyMetadata.IsKey.Should().Be(property.GetCustomAttribute() is not null); - propertyMetadata.IsRowVersion - .Should().Be(property.GetCustomAttribute() is not null); + propertyMetadata.IsRowVersion.Should().Be(property.GetCustomAttribute() is not null); if (propertyMetadata.CanRead) { - propertyMetadata.PropertyGetter - .Should().NotBeNull(); + propertyMetadata.PropertyGetter.Should().NotBeNull(); - propertyMetadata.PropertyGetter(entity) - .Should().Be(property.GetValue(entity)); + propertyMetadata.PropertyGetter(entity).Should().Be(property.GetValue(entity)); } else { - propertyMetadata.PropertyGetter - .Should().BeNull(); + propertyMetadata.PropertyGetter.Should().BeNull(); } - propertyMetadata.PropertyInfo - .Should().BeSameAs(property); + propertyMetadata.PropertyInfo.Should().BeSameAs(property); - propertyMetadata.PropertyName - .Should().Be(property.Name); + propertyMetadata.PropertyName.Should().Be(property.Name); if (propertyMetadata.CanWrite) { - propertyMetadata.PropertySetter - .Should().NotBeNull(); + propertyMetadata.PropertySetter.Should().NotBeNull(); var value = specimenFactoryCreateMethod .MakeGenericMethod(property.PropertyType) @@ -309,17 +311,14 @@ public void GetEntityTypeMetadata_Mapping_Attributes_ShouldGetMetadataBasedOnAtt propertyMetadata.PropertySetter(entity, value); - property.GetValue(entity) - .Should().Be(value); + property.GetValue(entity).Should().Be(value); } else { - propertyMetadata.PropertySetter - .Should().BeNull(); + propertyMetadata.PropertySetter.Should().BeNull(); } - propertyMetadata.PropertyType - .Should().Be(property.PropertyType); + propertyMetadata.PropertyType.Should().Be(property.PropertyType); } } @@ -330,160 +329,110 @@ public void GetEntityTypeMetadata_Mapping_FluentApi_ShouldGetMetadataBasedOnFlue var metadata = EntityHelper.GetEntityTypeMetadata(typeof(MappingTestEntityFluentApi)); - metadata - .Should().NotBeNull(); + metadata.Should().NotBeNull(); - metadata.EntityType - .Should().Be(typeof(MappingTestEntityFluentApi)); + metadata.EntityType.Should().Be(typeof(MappingTestEntityFluentApi)); - metadata.TableName - .Should().Be("MappingTestEntity"); + metadata.TableName.Should().Be("MappingTestEntity"); - metadata.AllProperties - .Should().HaveCount(8); + metadata.AllProperties.Should().HaveCount(8); - metadata.AllPropertiesByPropertyName - .Should().BeEquivalentTo(metadata.AllProperties.ToDictionary(a => a.PropertyName)); + metadata + .AllPropertiesByPropertyName.Should() + .BeEquivalentTo(metadata.AllProperties.ToDictionary(a => a.PropertyName)); var computedProperty = metadata.AllPropertiesByPropertyName["Computed_"]; - computedProperty.ColumnName - .Should().Be("Computed"); + computedProperty.ColumnName.Should().Be("Computed"); - computedProperty.IsComputed - .Should().BeTrue(); + computedProperty.IsComputed.Should().BeTrue(); var concurrencyTokenProperty = metadata.AllPropertiesByPropertyName["ConcurrencyToken_"]; - concurrencyTokenProperty.ColumnName - .Should().Be("ConcurrencyToken"); + concurrencyTokenProperty.ColumnName.Should().Be("ConcurrencyToken"); - concurrencyTokenProperty.IsConcurrencyToken - .Should().BeTrue(); + concurrencyTokenProperty.IsConcurrencyToken.Should().BeTrue(); var identityProperty = metadata.AllPropertiesByPropertyName["Identity_"]; - identityProperty.ColumnName - .Should().Be("Identity"); + identityProperty.ColumnName.Should().Be("Identity"); - identityProperty.IsIdentity - .Should().BeTrue(); + identityProperty.IsIdentity.Should().BeTrue(); var key1Property = metadata.AllPropertiesByPropertyName["Key1_"]; - key1Property.ColumnName - .Should().Be("Key1"); + key1Property.ColumnName.Should().Be("Key1"); - key1Property.IsKey - .Should().BeTrue(); + key1Property.IsKey.Should().BeTrue(); var key2Property = metadata.AllPropertiesByPropertyName["Key2_"]; - key2Property.ColumnName - .Should().Be("Key2"); + key2Property.ColumnName.Should().Be("Key2"); - key2Property.IsKey - .Should().BeTrue(); + key2Property.IsKey.Should().BeTrue(); var nameProperty = metadata.AllPropertiesByPropertyName["Value_"]; - nameProperty.ColumnName - .Should().Be("Value"); + nameProperty.ColumnName.Should().Be("Value"); var notMappedProperty = metadata.AllPropertiesByPropertyName["NotMapped"]; - notMappedProperty.IsIgnored - .Should().BeTrue(); + notMappedProperty.IsIgnored.Should().BeTrue(); var rowVersionProperty = metadata.AllPropertiesByPropertyName["RowVersion_"]; - rowVersionProperty.IsRowVersion - .Should().BeTrue(); - - metadata.ComputedProperties - .Should().BeEquivalentTo([computedProperty]); + rowVersionProperty.IsRowVersion.Should().BeTrue(); - metadata.ConcurrencyTokenProperties - .Should().BeEquivalentTo([concurrencyTokenProperty]); + metadata.ComputedProperties.Should().BeEquivalentTo([computedProperty]); - metadata.DatabaseGeneratedProperties - .Should().BeEquivalentTo([computedProperty, identityProperty, rowVersionProperty]); + metadata.ConcurrencyTokenProperties.Should().BeEquivalentTo([concurrencyTokenProperty]); - metadata.IdentityProperty - .Should().Be(identityProperty); - - metadata.InsertProperties - .Should().BeEquivalentTo([concurrencyTokenProperty, key1Property, key2Property, nameProperty]); + metadata + .DatabaseGeneratedProperties.Should() + .BeEquivalentTo([computedProperty, identityProperty, rowVersionProperty]); - metadata.KeyProperties - .Should().BeEquivalentTo([key1Property, key2Property]); + metadata.IdentityProperty.Should().Be(identityProperty); - metadata.MappedProperties - .Should().BeEquivalentTo( - [ - computedProperty, concurrencyTokenProperty, identityProperty, key1Property, key2Property, - nameProperty, rowVersionProperty - ] - ); + metadata + .InsertProperties.Should() + .BeEquivalentTo([concurrencyTokenProperty, key1Property, key2Property, nameProperty]); - metadata.RowVersionProperties - .Should().BeEquivalentTo( - [rowVersionProperty] - ); + metadata.KeyProperties.Should().BeEquivalentTo([key1Property, key2Property]); - metadata.UpdateProperties - .Should().BeEquivalentTo([nameProperty]); + metadata + .MappedProperties.Should() + .BeEquivalentTo([ + computedProperty, + concurrencyTokenProperty, + identityProperty, + key1Property, + key2Property, + nameProperty, + rowVersionProperty, + ]); + + metadata.RowVersionProperties.Should().BeEquivalentTo([rowVersionProperty]); + + metadata.UpdateProperties.Should().BeEquivalentTo([nameProperty]); } [Fact] public void GetEntityTypeMetadata_MoreThanOneIdentityProperty_ShouldThrow() => Invoking(() => EntityHelper.GetEntityTypeMetadata(typeof(EntityWithMultipleIdentityProperties))) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "There are multiple identity properties defined for the entity type " + - $"{typeof(EntityWithMultipleIdentityProperties)}. Only one property can be marked as an identity " + - "property per entity type." + "There are multiple identity properties defined for the entity type " + + $"{typeof(EntityWithMultipleIdentityProperties)}. Only one property can be marked as an identity " + + "property per entity type." ); - [Fact] - public void ShouldGuardAgainstNullArguments() - { - (String Name, Type Type)[] constructorParameters = - [("a", typeof(Int16)), ("b", typeof(Int32)), ("c", typeof(Int64))]; - - ArgumentNullGuardVerifier.Verify(() => - EntityHelper.FindCompatibleConstructor(typeof(ItemWithConstructor), constructorParameters) - ); - ArgumentNullGuardVerifier.Verify(() => EntityHelper.FindParameterlessConstructor(typeof(ItemWithConstructor))); - ArgumentNullGuardVerifier.Verify(() => EntityHelper.GetEntityTypeMetadata(typeof(Entity))); - } - - [Fact] - public void GetEntityTypeMetadata_ShouldNotInvokePropertyAccessorsWhileCreatingMetadata() - { - // Every accessor of this entity throws, so building its metadata would fail if the accessors were - // resolved and invoked eagerly. - var metadata = EntityHelper.GetEntityTypeMetadata(typeof(EntityWithThrowingAccessors)); - - var property = metadata.MappedProperties - .Single(p => p.PropertyName == nameof(EntityWithThrowingAccessors.Value)); - - property.PropertyGetter - .Should().NotBeNull(); - - // The accessor is real - it simply had not been called yet. - Invoking(() => property.PropertyGetter!(new EntityWithThrowingAccessors())) - .Should().Throw() - .WithMessage("Getter was invoked."); - } - [Fact] public void GetEntityTypeMetadata_PropertyAccessors_ShouldWorkAcrossRepeatedCalls() { var metadata = EntityHelper.GetEntityTypeMetadata(typeof(EntityWithNonPublicSetter)); - var property = metadata.MappedProperties - .Single(p => p.PropertyName == nameof(EntityWithNonPublicSetter.Value)); + var property = metadata.MappedProperties.Single(p => p.PropertyName == nameof(EntityWithNonPublicSetter.Value)); var entity = new EntityWithNonPublicSetter(); @@ -491,8 +440,7 @@ public void GetEntityTypeMetadata_PropertyAccessors_ShouldWorkAcrossRepeatedCall { property.PropertySetter!(entity, value); - property.PropertyGetter!(entity) - .Should().Be(value); + property.PropertyGetter!(entity).Should().Be(value); } } @@ -501,44 +449,59 @@ public void GetEntityTypeMetadata_ShouldCreateAccessorsForNonPublicAndInitOnlySe { var metadata = EntityHelper.GetEntityTypeMetadata(typeof(EntityWithNonPublicSetter)); - var privateSetterProperty = metadata.MappedProperties - .Single(p => p.PropertyName == nameof(EntityWithNonPublicSetter.Value)); + var privateSetterProperty = metadata.MappedProperties.Single(p => + p.PropertyName == nameof(EntityWithNonPublicSetter.Value) + ); - var initOnlyProperty = metadata.MappedProperties - .Single(p => p.PropertyName == nameof(EntityWithNonPublicSetter.Name)); + var initOnlyProperty = metadata.MappedProperties.Single(p => + p.PropertyName == nameof(EntityWithNonPublicSetter.Name) + ); var entity = new EntityWithNonPublicSetter(); privateSetterProperty.PropertySetter!(entity, 42); initOnlyProperty.PropertySetter!(entity, "Ada"); - entity.Value - .Should().Be(42); + entity.Value.Should().Be(42); - entity.Name - .Should().Be("Ada"); + entity.Name.Should().Be("Ada"); } - /// - /// The method. - /// - private static readonly MethodInfo specimenFactoryCreateMethod = typeof(SpecimenFactory) - .GetMethod( - nameof(SpecimenFactory.Create), - BindingFlags.Public | BindingFlags.Static, - [typeof(ISpecimenBuilder)] - )!; + [Fact] + public void GetEntityTypeMetadata_ShouldNotInvokePropertyAccessorsWhileCreatingMetadata() + { + // Every accessor of this entity throws, so building its metadata would fail if the accessors were + // resolved and invoked eagerly. + var metadata = EntityHelper.GetEntityTypeMetadata(typeof(EntityWithThrowingAccessors)); - /// - /// An entity whose property accessors throw, used to prove that creating metadata does not invoke them. - /// - private sealed class EntityWithThrowingAccessors + var property = metadata.MappedProperties.Single(p => + p.PropertyName == nameof(EntityWithThrowingAccessors.Value) + ); + + property.PropertyGetter.Should().NotBeNull(); + + // The accessor is real - it simply had not been called yet. + Invoking(() => property.PropertyGetter!(new EntityWithThrowingAccessors())) + .Should() + .Throw() + .WithMessage("Getter was invoked."); + } + + [Fact] + public void ShouldGuardAgainstNullArguments() { - public Int32 Value - { - get => throw new InvalidOperationException("Getter was invoked."); - set => throw new InvalidOperationException("Setter was invoked."); - } + (string Name, Type Type)[] constructorParameters = + [ + ("a", typeof(short)), + ("b", typeof(int)), + ("c", typeof(long)), + ]; + + ArgumentNullGuardVerifier.Verify(() => + EntityHelper.FindCompatibleConstructor(typeof(ItemWithConstructor), constructorParameters) + ); + ArgumentNullGuardVerifier.Verify(() => EntityHelper.FindParameterlessConstructor(typeof(ItemWithConstructor))); + ArgumentNullGuardVerifier.Verify(() => EntityHelper.GetEntityTypeMetadata(typeof(Entity))); } /// @@ -546,12 +509,24 @@ public Int32 Value /// private sealed class EntityWithNonPublicSetter { - public String? Name { get; init; } + public string? Name { get; init; } // The private setter is the point of this entity - it exists to be written through reflection, which // RCS1170 cannot see, so it believes the property should be read-only. #pragma warning disable RCS1170 - public Int32 Value { get; private set; } + public int Value { get; private set; } #pragma warning restore RCS1170 } + + /// + /// An entity whose property accessors throw, used to prove that creating metadata does not invoke them. + /// + private sealed class EntityWithThrowingAccessors + { + public int Value + { + get => throw new InvalidOperationException("Getter was invoked."); + set => throw new InvalidOperationException("Setter was invoked."); + } + } } diff --git a/tests/DbConnectionPlus.UnitTests/Extensions/DbDataReaderExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/Extensions/DbDataReaderExtensionsTests.cs index 07e35cf..e5b1b1b 100644 --- a/tests/DbConnectionPlus.UnitTests/Extensions/DbDataReaderExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Extensions/DbDataReaderExtensionsTests.cs @@ -9,7 +9,7 @@ public class DbDataReaderExtensionsTests : UnitTestsBase [Fact] public void GetFieldNames_ShouldReturnFieldNames() { - String[] fieldNames = ["FieldA", "FieldB", "FieldC"]; + string[] fieldNames = ["FieldA", "FieldB", "FieldC"]; var dataReader = Substitute.For(); @@ -19,14 +19,13 @@ public void GetFieldNames_ShouldReturnFieldNames() dataReader.GetName(1).Returns(fieldNames[1]); dataReader.GetName(2).Returns(fieldNames[2]); - dataReader.GetFieldNames() - .Should().BeEquivalentTo(fieldNames); + dataReader.GetFieldNames().Should().BeEquivalentTo(fieldNames); } [Fact] public void GetFieldTypes_ShouldReturnFieldTypes() { - Type[] fieldTypes = [typeof(Int32), typeof(String), typeof(DateTime)]; + Type[] fieldTypes = [typeof(int), typeof(string), typeof(DateTime)]; var dataReader = Substitute.For(); @@ -36,8 +35,7 @@ public void GetFieldTypes_ShouldReturnFieldTypes() dataReader.GetFieldType(1).Returns(fieldTypes[1]); dataReader.GetFieldType(2).Returns(fieldTypes[2]); - dataReader.GetFieldTypes() - .Should().BeEquivalentTo(fieldTypes); + dataReader.GetFieldTypes().Should().BeEquivalentTo(fieldTypes); } [Fact] diff --git a/tests/DbConnectionPlus.UnitTests/Extensions/Int32ExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/Extensions/Int32ExtensionsTests.cs index a31dc9c..ec1b5a2 100644 --- a/tests/DbConnectionPlus.UnitTests/Extensions/Int32ExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Extensions/Int32ExtensionsTests.cs @@ -16,7 +16,6 @@ public class Int32ExtensionsTests : UnitTestsBase [InlineData(23, "23rd")] [InlineData(24, "24th")] [InlineData(25, "25th")] - public void OrdinalizeEnglish_ShouldOrdinalizeNumberInEnglishFormat(Int32 number, String expectedResult) => - number.OrdinalizeEnglish() - .Should().Be(expectedResult); + public void OrdinalizeEnglish_ShouldOrdinalizeNumberInEnglishFormat(int number, string expectedResult) => + number.OrdinalizeEnglish().Should().Be(expectedResult); } diff --git a/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs index 582dde3..f1b1e14 100644 --- a/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Extensions/ObjectExtensionsTests.cs @@ -9,140 +9,139 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Extensions; public class ObjectExtensionsTests : UnitTestsBase { - [Fact] - public void ToDebugString_ShouldUseToStringForUnhandledTypes() => new Item("A").ToDebugString() - .Should().Be( - "'Item A' (RentADeveloper.DbConnectionPlus.UnitTests.Extensions.ObjectExtensionsTests+Item)" - ); - [Fact] public void ToDebugString_ShouldRenderSequencesElementByElement() { - new List { "A", "B" }.ToDebugString() - .Should().Be("'[A,B]' (System.Collections.Generic.List`1[System.String])"); - - new Object?[] { 1, null, "A", true }.ToDebugString() - .Should().Be("'[1,{null},A,True]' (System.Object[])"); - - new Int32[][] { [1, 2], [3] }.ToDebugString() - .Should().Be("'[[1,2],[3]]' (System.Int32[][])"); - - Array.Empty().ToDebugString() - .Should().Be("'[]' (System.Int32[])"); - } - - [Fact] - public void ToDebugString_ShouldTruncateSelfReferencingSequencesInsteadOfRecursingForever() - { - var values = new List { 1 }; - - values.Add(values); - - // The depth bound replaces the cycle handling that the previous JsonSerializer-based implementation got - // from ReferenceHandler.IgnoreCycles. What matters is that this terminates at all; the exact nesting - // depth at which it stops is an implementation detail. - var debugString = values.ToDebugString(); - - debugString.Should().StartWith("'[1,[1,[1,"); - debugString.Should().Contain("[...]"); + new List { "A", "B" } + .ToDebugString() + .Should() + .Be("'[A,B]' (System.Collections.Generic.List`1[System.String])"); + + new object?[] { 1, null, "A", true } + .ToDebugString() + .Should() + .Be("'[1,{null},A,True]' (System.Object[])"); + + new int[][] { [1, 2], [3] } + .ToDebugString() + .Should() + .Be("'[[1,2],[3]]' (System.Int32[][])"); + + Array.Empty().ToDebugString().Should().Be("'[]' (System.Int32[])"); } [Fact] public void ToDebugString_ShouldReturnStringRepresentationOfValue() { #pragma warning disable RCS1202 - (null as Object).ToDebugString() - .Should().Be("{null}"); + (null as object).ToDebugString().Should().Be("{null}"); #pragma warning restore RCS1202 - DBNull.Value.ToDebugString() - .Should().Be("{DBNull}"); + DBNull.Value.ToDebugString().Should().Be("{DBNull}"); - true.ToDebugString() - .Should().Be("'True' (System.Boolean)"); + true.ToDebugString().Should().Be("'True' (System.Boolean)"); - ((Byte)123).ToDebugString() - .Should().Be("'123' (System.Byte)"); + ((byte)123).ToDebugString().Should().Be("'123' (System.Byte)"); - new Byte[] { 1, 2, 3 }.ToDebugString() - .Should().Be("'AQID' (System.Byte[])"); + new byte[] { 1, 2, 3 } + .ToDebugString() + .Should() + .Be("'AQID' (System.Byte[])"); - 'X'.ToDebugString() - .Should().Be("'X' (System.Char)"); + 'X'.ToDebugString().Should().Be("'X' (System.Char)"); - new DateTime(2025, 12, 31, 23, 59, 59, 999).ToDebugString() - .Should().Be("'2025-12-31T23:59:59.9990000' (System.DateTime)"); + new DateTime(2025, 12, 31, 23, 59, 59, 999) + .ToDebugString() + .Should() + .Be("'2025-12-31T23:59:59.9990000' (System.DateTime)"); - new DateTimeOffset(2025, 12, 31, 23, 59, 59, TimeSpan.FromHours(1)).ToDebugString() - .Should().Be("'2025-12-31T23:59:59.0000000+01:00' (System.DateTimeOffset)"); + new DateTimeOffset(2025, 12, 31, 23, 59, 59, TimeSpan.FromHours(1)) + .ToDebugString() + .Should() + .Be("'2025-12-31T23:59:59.0000000+01:00' (System.DateTimeOffset)"); - 123.45M.ToDebugString() - .Should().Be("'123.45' (System.Decimal)"); + 123.45M.ToDebugString().Should().Be("'123.45' (System.Decimal)"); - 123.45.ToDebugString() - .Should().Be("'123.45' (System.Double)"); + 123.45.ToDebugString().Should().Be("'123.45' (System.Double)"); - TestEnum.Value3.ToDebugString() - .Should().Be("'Value3' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.TestEnum)"); + TestEnum + .Value3.ToDebugString() + .Should() + .Be("'Value3' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.TestEnum)"); - new Guid("889a8be0-f0ff-4555-86d8-8490434b7def").ToDebugString() - .Should().Be("'889a8be0-f0ff-4555-86d8-8490434b7def' (System.Guid)"); + new Guid("889a8be0-f0ff-4555-86d8-8490434b7def") + .ToDebugString() + .Should() + .Be("'889a8be0-f0ff-4555-86d8-8490434b7def' (System.Guid)"); - ((Int16)123).ToDebugString() - .Should().Be("'123' (System.Int16)"); + ((short)123).ToDebugString().Should().Be("'123' (System.Int16)"); - 123.ToDebugString() - .Should().Be("'123' (System.Int32)"); + 123.ToDebugString().Should().Be("'123' (System.Int32)"); - ((Int64)123).ToDebugString() - .Should().Be("'123' (System.Int64)"); + ((long)123).ToDebugString().Should().Be("'123' (System.Int64)"); - ((IntPtr)123).ToDebugString() - .Should().Be("'123' (System.IntPtr)"); + ((nint)123).ToDebugString().Should().Be("'123' (System.IntPtr)"); - ((SByte)123).ToDebugString() - .Should().Be("'123' (System.SByte)"); + ((sbyte)123).ToDebugString().Should().Be("'123' (System.SByte)"); - ((Single)123.45).ToDebugString() - .Should().Be("'123.449997' (System.Single)"); + ((float)123.45).ToDebugString().Should().Be("'123.449997' (System.Single)"); - "A String".ToDebugString() - .Should().Be("'A String' (System.String)"); + "A String".ToDebugString().Should().Be("'A String' (System.String)"); - new TimeSpan(1, 2, 3, 4).ToDebugString() - .Should().Be("'1.02:03:04' (System.TimeSpan)"); + new TimeSpan(1, 2, 3, 4).ToDebugString().Should().Be("'1.02:03:04' (System.TimeSpan)"); - ((UInt16)123).ToDebugString() - .Should().Be("'123' (System.UInt16)"); + ((ushort)123).ToDebugString().Should().Be("'123' (System.UInt16)"); - ((UInt32)123).ToDebugString() - .Should().Be("'123' (System.UInt32)"); + ((uint)123).ToDebugString().Should().Be("'123' (System.UInt32)"); - ((UInt64)123).ToDebugString() - .Should().Be("'123' (System.UInt64)"); + ((ulong)123).ToDebugString().Should().Be("'123' (System.UInt64)"); - ((UIntPtr)123).ToDebugString() - .Should().Be("'123' (System.UIntPtr)"); + ((nuint)123).ToDebugString().Should().Be("'123' (System.UIntPtr)"); #pragma warning disable CA1861 // Avoid constant arrays as arguments - new Int32[] { 1, 2, 3 }.ToDebugString() - .Should().Be("'[1,2,3]' (System.Int32[])"); + new int[] { 1, 2, 3 } + .ToDebugString() + .Should() + .Be("'[1,2,3]' (System.Int32[])"); #pragma warning restore CA1861 // Avoid constant arrays as arguments - new Object().ToDebugString() - .Should().Be("'System.Object' (System.Object)"); + new object().ToDebugString().Should().Be("'System.Object' (System.Object)"); - new EntityWithEnumStoredAsString { Enum = TestEnum.Value3, Id = 1 }.ToDebugString() - .Should().Be( - "'EntityWithEnumStoredAsString { Enum = Value3, Id = 1 }' " + - "(RentADeveloper.DbConnectionPlus.UnitTests.TestData.EntityWithEnumStoredAsString)" + new EntityWithEnumStoredAsString { Enum = TestEnum.Value3, Id = 1 } + .ToDebugString() + .Should() + .Be( + "'EntityWithEnumStoredAsString { Enum = Value3, Id = 1 }' " + + "(RentADeveloper.DbConnectionPlus.UnitTests.TestData.EntityWithEnumStoredAsString)" ); } - private sealed class Item(String id) + [Fact] + public void ToDebugString_ShouldTruncateSelfReferencingSequencesInsteadOfRecursingForever() + { + var values = new List { 1 }; + + values.Add(values); + + // The depth bound replaces the cycle handling that the previous JsonSerializer-based implementation got + // from ReferenceHandler.IgnoreCycles. What matters is that this terminates completely; the exact nesting + // depth at which it stops is an implementation detail. + var debugString = values.ToDebugString(); + + debugString.Should().StartWith("'[1,[1,[1,"); + debugString.Should().Contain("[...]"); + } + + [Fact] + public void ToDebugString_ShouldUseToStringForUnhandledTypes() => + new Item("A") + .ToDebugString() + .Should() + .Be("'Item A' (RentADeveloper.DbConnectionPlus.UnitTests.Extensions.ObjectExtensionsTests+Item)"); + + private sealed class Item(string id) { /// - public override String ToString() => - $"Item {id}"; + public override string ToString() => $"Item {id}"; } } diff --git a/tests/DbConnectionPlus.UnitTests/Extensions/TypeExtensionsTests.cs b/tests/DbConnectionPlus.UnitTests/Extensions/TypeExtensionsTests.cs index ce9d29e..a493591 100644 --- a/tests/DbConnectionPlus.UnitTests/Extensions/TypeExtensionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Extensions/TypeExtensionsTests.cs @@ -8,129 +8,117 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Extensions; public class TypeExtensionsTests : UnitTestsBase { [Theory] - [InlineData(typeof(Boolean), true)] - [InlineData(typeof(Boolean?), true)] - [InlineData(typeof(Byte), true)] - [InlineData(typeof(Byte?), true)] - [InlineData(typeof(Char), true)] - [InlineData(typeof(Char?), true)] + [InlineData(typeof(bool), true)] + [InlineData(typeof(bool?), true)] + [InlineData(typeof(byte), true)] + [InlineData(typeof(byte?), true)] + [InlineData(typeof(char), true)] + [InlineData(typeof(char?), true)] [InlineData(typeof(DateOnly), true)] [InlineData(typeof(DateOnly?), true)] [InlineData(typeof(DateTime), true)] [InlineData(typeof(DateTime?), true)] [InlineData(typeof(DateTimeOffset), true)] [InlineData(typeof(DateTimeOffset?), true)] - [InlineData(typeof(Decimal), true)] - [InlineData(typeof(Decimal?), true)] - [InlineData(typeof(Double), true)] - [InlineData(typeof(Double?), true)] + [InlineData(typeof(decimal), true)] + [InlineData(typeof(decimal?), true)] + [InlineData(typeof(double), true)] + [InlineData(typeof(double?), true)] [InlineData(typeof(Guid), true)] [InlineData(typeof(Guid?), true)] - [InlineData(typeof(Int16), true)] - [InlineData(typeof(Int16?), true)] - [InlineData(typeof(Int32), true)] - [InlineData(typeof(Int32?), true)] - [InlineData(typeof(Int64), true)] - [InlineData(typeof(Int64?), true)] - [InlineData(typeof(IntPtr), true)] - [InlineData(typeof(IntPtr?), true)] - [InlineData(typeof(SByte), true)] - [InlineData(typeof(SByte?), true)] - [InlineData(typeof(Single), true)] - [InlineData(typeof(Single?), true)] - [InlineData(typeof(String), true)] + [InlineData(typeof(short), true)] + [InlineData(typeof(short?), true)] + [InlineData(typeof(int), true)] + [InlineData(typeof(int?), true)] + [InlineData(typeof(long), true)] + [InlineData(typeof(long?), true)] + [InlineData(typeof(nint), true)] + [InlineData(typeof(nint?), true)] + [InlineData(typeof(sbyte), true)] + [InlineData(typeof(sbyte?), true)] + [InlineData(typeof(float), true)] + [InlineData(typeof(float?), true)] + [InlineData(typeof(string), true)] [InlineData(typeof(TimeOnly), true)] [InlineData(typeof(TimeOnly?), true)] [InlineData(typeof(TimeSpan), true)] [InlineData(typeof(TimeSpan?), true)] - [InlineData(typeof(UInt16), true)] - [InlineData(typeof(UInt16?), true)] - [InlineData(typeof(UInt32), true)] - [InlineData(typeof(UInt32?), true)] - [InlineData(typeof(UInt64), true)] - [InlineData(typeof(UInt64?), true)] - [InlineData(typeof(UIntPtr), true)] - [InlineData(typeof(UIntPtr?), true)] + [InlineData(typeof(ushort), true)] + [InlineData(typeof(ushort?), true)] + [InlineData(typeof(uint), true)] + [InlineData(typeof(uint?), true)] + [InlineData(typeof(ulong), true)] + [InlineData(typeof(ulong?), true)] + [InlineData(typeof(nuint), true)] + [InlineData(typeof(nuint?), true)] [InlineData(typeof(Entity), false)] [InlineData(typeof(TestEnum), false)] public void IsBuiltInTypeOrNullableBuiltInType_ShouldDetermineWhetherTypeIsBuiltInTypeOrNullableBuiltInType( Type type, - Boolean expectedResult - ) => - type.IsBuiltInTypeOrNullableBuiltInType() - .Should().Be(expectedResult); + bool expectedResult + ) => type.IsBuiltInTypeOrNullableBuiltInType().Should().Be(expectedResult); [Theory] - [InlineData(typeof(Char), true)] - [InlineData(typeof(Char?), true)] - [InlineData(typeof(String), false)] + [InlineData(typeof(char), true)] + [InlineData(typeof(char?), true)] + [InlineData(typeof(string), false)] [InlineData(typeof(DateTime), false)] [InlineData(typeof(Entity), false)] public void IsCharOrNullableCharType_ShouldDetermineWhetherTypeIsCharOrNullableCharType( Type type, - Boolean expectedResult - ) => - type.IsCharOrNullableCharType() - .Should().Be(expectedResult); + bool expectedResult + ) => type.IsCharOrNullableCharType().Should().Be(expectedResult); [Theory] [InlineData(typeof(TestEnum), true)] [InlineData(typeof(TestEnum?), true)] [InlineData(typeof(ConsoleColor), true)] [InlineData(typeof(ConsoleColor?), true)] - [InlineData(typeof(String), false)] + [InlineData(typeof(string), false)] [InlineData(typeof(DateTime), false)] [InlineData(typeof(Entity), false)] public void IsEnumOrNullableEnumType_ShouldDetermineWhetherTypeIsEnumTypeOrNullableEnumType( Type type, - Boolean expectedResult - ) => - type.IsEnumOrNullableEnumType() - .Should().Be(expectedResult); + bool expectedResult + ) => type.IsEnumOrNullableEnumType().Should().Be(expectedResult); [Theory] - [InlineData(typeof(Int32?), true)] + [InlineData(typeof(int?), true)] [InlineData(typeof(DateTime?), true)] - [InlineData(typeof(Object), true)] + [InlineData(typeof(object), true)] [InlineData(typeof(Entity), true)] - [InlineData(typeof(Int32[]), true)] - [InlineData(typeof(String[]), true)] - [InlineData(typeof(Int32), false)] + [InlineData(typeof(int[]), true)] + [InlineData(typeof(string[]), true)] + [InlineData(typeof(int), false)] [InlineData(typeof(DateTime), false)] public void IsReferenceTypeOrNullableType_ShouldDetermineWhetherTypeIsReferenceTypeOrNullableType( Type type, - Boolean expectedResult - ) => - type.IsReferenceTypeOrNullableType() - .Should().Be(expectedResult); + bool expectedResult + ) => type.IsReferenceTypeOrNullableType().Should().Be(expectedResult); [Theory] - [InlineData(typeof(ValueTuple), true)] - [InlineData(typeof(ValueTuple), true)] - [InlineData(typeof(ValueTuple), true)] - [InlineData(typeof(ValueTuple), true)] - [InlineData(typeof(ValueTuple), true)] - [InlineData(typeof(ValueTuple), true)] - [InlineData(typeof(ValueTuple), true)] - [InlineData(typeof(ValueTuple), true)] + [InlineData(typeof(ValueTuple), true)] + [InlineData(typeof(ValueTuple), true)] + [InlineData(typeof(ValueTuple), true)] + [InlineData(typeof(ValueTuple), true)] + [InlineData(typeof(ValueTuple), true)] + [InlineData(typeof(ValueTuple), true)] + [InlineData(typeof(ValueTuple), true)] + [InlineData(typeof(ValueTuple), true)] [InlineData(typeof(DateTime), false)] [InlineData(typeof(Entity), false)] - [InlineData(typeof(Tuple), false)] - [InlineData(typeof(Tuple), false)] - public void IsValueTupleType_ShouldDetermineWhetherTypeIsValueTupleType( - Type type, - Boolean expectedResult - ) => - type.IsValueTupleType() - .Should().Be(expectedResult); + [InlineData(typeof(Tuple), false)] + [InlineData(typeof(Tuple), false)] + public void IsValueTupleType_ShouldDetermineWhetherTypeIsValueTupleType(Type type, bool expectedResult) => + type.IsValueTupleType().Should().Be(expectedResult); [Fact] public void ShouldGuardAgainstNullArguments() { ArgumentNullGuardVerifier.Verify(() => TypeExtensions.IsBuiltInTypeOrNullableBuiltInType(typeof(DateTime))); - ArgumentNullGuardVerifier.Verify(() => TypeExtensions.IsCharOrNullableCharType(typeof(Char))); + ArgumentNullGuardVerifier.Verify(() => TypeExtensions.IsCharOrNullableCharType(typeof(char))); ArgumentNullGuardVerifier.Verify(() => TypeExtensions.IsEnumOrNullableEnumType(typeof(TestEnum))); ArgumentNullGuardVerifier.Verify(() => TypeExtensions.IsReferenceTypeOrNullableType(typeof(Entity))); - ArgumentNullGuardVerifier.Verify(() => TypeExtensions.IsValueTupleType(typeof(ValueTuple))); + ArgumentNullGuardVerifier.Verify(() => TypeExtensions.IsValueTupleType(typeof(ValueTuple))); } } diff --git a/tests/DbConnectionPlus.UnitTests/GlobalUsings.cs b/tests/DbConnectionPlus.UnitTests/GlobalUsings.cs index 878017e..5e88c20 100644 --- a/tests/DbConnectionPlus.UnitTests/GlobalUsings.cs +++ b/tests/DbConnectionPlus.UnitTests/GlobalUsings.cs @@ -3,12 +3,11 @@ global using System.ComponentModel.DataAnnotations.Schema; global using System.Data; global using System.Data.Common; -global using Xunit; global using AwesomeAssertions; +global using static AwesomeAssertions.FluentActions; global using Microsoft.Data.SqlClient; global using NSubstitute; global using RentADeveloper.ArgumentNullGuards; global using RentADeveloper.DbConnectionPlus.Configuration; -global using RentADeveloper.DbConnectionPlus.UnitTests.TestData; -global using static AwesomeAssertions.FluentActions; global using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; +global using RentADeveloper.DbConnectionPlus.UnitTests.TestData; diff --git a/tests/DbConnectionPlus.UnitTests/Helpers/NameHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Helpers/NameHelperTests.cs index b537fe9..a615994 100644 --- a/tests/DbConnectionPlus.UnitTests/Helpers/NameHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Helpers/NameHelperTests.cs @@ -19,10 +19,8 @@ public class NameHelperTests : UnitTestsBase [InlineData("..........1234567890", 10, "1234567890")] [InlineData(".....12345.....67890", 10, "1234567890")] public void CreateNameFromCallerArgumentExpression_ShouldCreateName( - String expression, - Int32 maximumLength, - String expectedName - ) => - NameHelper.CreateNameFromCallerArgumentExpression(expression, maximumLength) - .Should().Be(expectedName); + string expression, + int maximumLength, + string expectedName + ) => NameHelper.CreateNameFromCallerArgumentExpression(expression, maximumLength).Should().Be(expectedName); } diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/DataRowMaterializerTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/DataRowMaterializerTests.cs index 898d9b2..704acc6 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/DataRowMaterializerTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/DataRowMaterializerTests.cs @@ -20,32 +20,26 @@ public void Materialize_ReturnsDataRowWithAllColumnsAndValues() dataReader.GetName(2).Returns("ColumnC"); dataReader - .GetValues(Arg.Any()) + .GetValues(Arg.Any()) .Returns(callInfo => - { - var array = callInfo.Arg(); - array[0] = value1; - array[1] = value2; - array[2] = value3; - return 3; - } - ); + { + var array = callInfo.Arg(); + array[0] = value1; + array[1] = value2; + array[2] = value3; + return 3; + }); var dataRow = DataRowMaterializer.Materialize(dataReader); - dataRow - .Should().Contain("ColumnA", value1); + dataRow.Should().Contain("ColumnA", value1); - dataRow - .Should().Contain("ColumnB", value2); + dataRow.Should().Contain("ColumnB", value2); - dataRow - .Should().Contain("ColumnC", value3); + dataRow.Should().Contain("ColumnC", value3); } [Fact] public void ShouldGuardAgainstNullArguments() => - ArgumentNullGuardVerifier.Verify(() => - DataRowMaterializer.Materialize(Substitute.For()) - ); + ArgumentNullGuardVerifier.Verify(() => DataRowMaterializer.Materialize(Substitute.For())); } diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs index c57443d..f5f38aa 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/EntityMaterializerFactoryTests.cs @@ -18,14 +18,15 @@ public void GetMaterializer_DataReaderFieldHasNoName_ShouldThrow() dataReader.FieldCount.Returns(1); - dataReader.GetFieldType(0).Returns(typeof(String)); - dataReader.GetName(0).Returns(String.Empty); + dataReader.GetFieldType(0).Returns(typeof(string)); + dataReader.GetName(0).Returns(string.Empty); Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + - "statement returns have a name.*" + "The 1st column returned by the SQL statement does not have a name. Make sure that all columns the " + + "statement returns have a name.*" ); } @@ -40,11 +41,12 @@ public void GetMaterializer_DataReaderFieldTypeNotCompatibleWithEntityPropertyTy dataReader.GetFieldType(0).Returns(typeof(Guid)); Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - $"The data type {typeof(Guid)} of the column 'CharValue' returned by the SQL statement is not " + - $"compatible with the property type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}.*" + $"The data type {typeof(Guid)} of the column 'CharValue' returned by the SQL statement is not " + + $"compatible with the property type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}.*" ); } @@ -56,124 +58,124 @@ public void GetMaterializer_DataReaderHasNoFields_ShouldThrow() dataReader.FieldCount.Returns(0); Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage("The SQL statement did not return any columns.*"); } [Fact] - public void GetMaterializer_NoFieldMatchesAWritableProperty_ShouldThrow() + public void GetMaterializer_DataReaderHasUnsupportedFieldType_ShouldThrow() { var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(2); - - dataReader.GetName(0).Returns("NotAPropertyOfEntity"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.FieldCount.Returns(1); - dataReader.GetName(1).Returns("AlsoNotAPropertyOfEntity"); - dataReader.GetFieldType(1).Returns(typeof(Int32)); + dataReader.GetName(0).Returns("Value"); + dataReader.GetFieldType(0).Returns(typeof(BigInteger)); - Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) - .Should().Throw() + Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) + .Should() + .Throw() .WithMessage( - "None of the 2 field(s) of the result set (NotAPropertyOfEntity, AlsoNotAPropertyOfEntity) could " + - $"be mapped to a writable property of the entity type {typeof(Entity)}.*" + $"The data type {typeof(BigInteger)} of the column 'Value' returned by the SQL statement is not " + + "supported.*" ); } [Fact] - public void GetMaterializer_SomeFieldsMatchAWritableProperty_ShouldNotThrow() + public void GetMaterializer_NoFieldMatchesAWritableProperty_ShouldThrow() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(2); - dataReader.GetName(0).Returns("CharValue"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetName(0).Returns("NotAPropertyOfEntity"); + dataReader.GetFieldType(0).Returns(typeof(string)); - dataReader.GetName(1).Returns("NotAPropertyOfEntity"); - dataReader.GetFieldType(1).Returns(typeof(String)); + dataReader.GetName(1).Returns("AlsoNotAPropertyOfEntity"); + dataReader.GetFieldType(1).Returns(typeof(int)); Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) - .Should().NotThrow(); + .Should() + .Throw() + .WithMessage( + "None of the 2 field(s) of the result set (NotAPropertyOfEntity, AlsoNotAPropertyOfEntity) could " + + $"be mapped to a writable property of the entity type {typeof(Entity)}.*" + ); } [Fact] - public void GetMaterializer_DataReaderHasUnsupportedFieldType_ShouldThrow() + public void GetMaterializer_SomeFieldsMatchAWritableProperty_ShouldNotThrow() { var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(1); + dataReader.FieldCount.Returns(2); - dataReader.GetName(0).Returns("Value"); - dataReader.GetFieldType(0).Returns(typeof(BigInteger)); + dataReader.GetName(0).Returns("CharValue"); + dataReader.GetFieldType(0).Returns(typeof(string)); - Invoking(() => - EntityMaterializerFactory.GetMaterializer(dataReader) - ) - .Should().Throw() - .WithMessage( - $"The data type {typeof(BigInteger)} of the column 'Value' returned by the SQL statement is not " + - "supported.*" - ); + dataReader.GetName(1).Returns("NotAPropertyOfEntity"); + dataReader.GetFieldType(1).Returns(typeof(string)); + + Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)).Should().NotThrow(); } [Fact] - public void - Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() + public void Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("CharValue"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(String.Empty); + dataReader.GetString(0).Returns(string.Empty); var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly one " + - "character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly one " + + "character long." ); dataReader.GetString(0).Returns("ab"); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } [Fact] - public void - Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() + public void Materializer_CharEntityProperty_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); - var character = Generate.Single(); + var character = Generate.Single(); dataReader.GetName(0).Returns("CharValue"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns(character.ToString()); @@ -181,8 +183,7 @@ public void var entity = materializer(dataReader); - entity.CharValue - .Should().Be(character); + entity.CharValue.Should().Be(character); } [Fact] @@ -198,8 +199,7 @@ public void Materializer_CompatiblePrivateConstructor_ShouldUsePrivateConstructo var materializedEntity = materializer(dataReader); - materializedEntity - .Should().BeEquivalentTo(entities[0]); + materializedEntity.Should().BeEquivalentTo(entities[0]); } [Fact] @@ -215,8 +215,7 @@ public void Materializer_CompatiblePublicConstructor_ShouldUsePublicConstructor( var materializedEntity = materializer(dataReader); - materializedEntity - .Should().BeEquivalentTo(entities[0]); + materializedEntity.Should().BeEquivalentTo(entities[0]); } [Fact] @@ -227,7 +226,7 @@ public void Materializer_DataReaderFieldNameMatchesEntityPropertyCaseInsensitive dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("id"); // lower-case - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.IsDBNull(0).Returns(false); dataReader.GetInt64(0).Returns(789); @@ -235,8 +234,7 @@ public void Materializer_DataReaderFieldNameMatchesEntityPropertyCaseInsensitive var entity = materializer(dataReader); - entity.Id - .Should().Be(789); + entity.Id.Should().Be(789); } [Fact] @@ -250,24 +248,22 @@ public void Materializer_DataReaderHasCompatibleFieldTypes_ShouldConvertValues() dataReader.FieldCount.Returns(2); dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(String)); // EntityWithEnumStoredAsInteger.Id is of type Int64. + dataReader.GetFieldType(0).Returns(typeof(string)); // EntityWithEnumStoredAsInteger.Id is of type Int64. dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns(entityId.ToString()); dataReader.GetName(1).Returns("Enum"); - dataReader.GetFieldType(1).Returns(typeof(Decimal)); // EntityWithEnumStoredAsInteger.Enum is of type TestEnum. + dataReader.GetFieldType(1).Returns(typeof(decimal)); // EntityWithEnumStoredAsInteger.Enum is of type TestEnum. dataReader.IsDBNull(1).Returns(false); - dataReader.GetDecimal(1).Returns((Decimal)enumValue); + dataReader.GetDecimal(1).Returns((decimal)enumValue); var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); var entity = materializer(dataReader); - entity.Id - .Should().Be(entityId); + entity.Id.Should().Be(entityId); - entity.Enum - .Should().Be(enumValue); + entity.Enum.Should().Be(enumValue); } [Fact] @@ -276,92 +272,68 @@ public void Materializer_EntityHasNoCorrespondingPropertyForDataReaderField_Shou var dataReader = Substitute.For(); var id = Generate.Id(); - var value = Generate.Single(); + var value = Generate.Single(); dataReader.FieldCount.Returns(3); - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.GetName(0).Returns("Id"); dataReader.IsDBNull(0).Returns(false); dataReader.GetInt64(0).Returns(id); - dataReader.GetFieldType(1).Returns(typeof(Int32)); + dataReader.GetFieldType(1).Returns(typeof(int)); dataReader.GetName(1).Returns("Int32Value"); dataReader.IsDBNull(1).Returns(false); dataReader.GetInt32(1).Returns(value); - dataReader.GetFieldType(2).Returns(typeof(Int32)); + dataReader.GetFieldType(2).Returns(typeof(int)); dataReader.GetName(2).Returns("NonExistent"); dataReader.IsDBNull(2).Returns(false); dataReader.GetInt64(2).Returns(Generate.SmallNumber()); - var materializer = Invoking(() => - EntityMaterializerFactory.GetMaterializer(dataReader) - ) - .Should().NotThrow().Subject; + var materializer = Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) + .Should() + .NotThrow() + .Subject; var entity = materializer(dataReader); - entity.Id - .Should().Be(id); + entity.Id.Should().Be(id); - entity.Int32Value - .Should().Be(value); + entity.Int32Value.Should().Be(value); } [Fact] - public void Materializer_EnumEntityProperty_DataReaderFieldContainsInteger_ShouldConvertToEnumMember() + public void Materializer_EnumEntityProperty_DataReaderFieldContainsIntegerNotMatchingAnyEnumMemberValue_ShouldThrow() { var dataReader = Substitute.For(); - var enumValue = Generate.Single(); - dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(Int32)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetInt32(0).Returns((Int32)enumValue); - - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); - - var entity = materializer(dataReader); - - entity.Enum - .Should().Be(enumValue); - } - - [Fact] - public void - Materializer_EnumEntityProperty_DataReaderFieldContainsIntegerNotMatchingAnyEnumMemberValue_ShouldThrow() - { - var dataReader = Substitute.For(); - - dataReader.FieldCount.Returns(1); - - dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(Int32)); + dataReader.GetFieldType(0).Returns(typeof(int)); dataReader.IsDBNull(0).Returns(false); dataReader.GetInt32(0).Returns(999); var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsInteger)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); } [Fact] - public void Materializer_EnumEntityProperty_DataReaderFieldContainsString_ShouldConvertToEnumMember() + public void Materializer_EnumEntityProperty_DataReaderFieldContainsInteger_ShouldConvertToEnumMember() { var dataReader = Substitute.For(); @@ -370,16 +342,15 @@ public void Materializer_EnumEntityProperty_DataReaderFieldContainsString_Should dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(int)); dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(enumValue.ToString()); + dataReader.GetInt32(0).Returns((int)enumValue); - var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); var entity = materializer(dataReader); - entity.Enum - .Should().Be(enumValue); + entity.Enum.Should().Be(enumValue); } [Fact] @@ -390,26 +361,48 @@ public void Materializer_EnumEntityProperty_DataReaderFieldContainsStringNotMatc dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns("NonExistent"); var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding property of the type " + - $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding property of the type " + + $"{typeof(EntityWithEnumStoredAsString)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. That " + - "string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. That " + + "string does not match any of the names of the enum's members.*" ); } + [Fact] + public void Materializer_EnumEntityProperty_DataReaderFieldContainsString_ShouldConvertToEnumMember() + { + var dataReader = Substitute.For(); + + var enumValue = Generate.Single(); + + dataReader.FieldCount.Returns(1); + + dataReader.GetName(0).Returns("Enum"); + dataReader.GetFieldType(0).Returns(typeof(string)); + dataReader.IsDBNull(0).Returns(false); + dataReader.GetString(0).Returns(enumValue.ToString()); + + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); + + var entity = materializer(dataReader); + + entity.Enum.Should().Be(enumValue); + } + [Fact] public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() { @@ -421,48 +414,48 @@ public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() var ordinal = 0; dataReader.GetName(ordinal).Returns("Computed"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Computed_); ordinal++; dataReader.GetName(ordinal).Returns("ConcurrencyToken"); - dataReader.GetFieldType(ordinal).Returns(typeof(Byte[])); + dataReader.GetFieldType(ordinal).Returns(typeof(byte[])); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetValue(ordinal).Returns(entity.ConcurrencyToken_); ordinal++; dataReader.GetName(ordinal).Returns("Identity"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Identity_); ordinal++; dataReader.GetName(ordinal).Returns("Key1"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt64(ordinal).Returns(entity.Key1_); ordinal++; dataReader.GetName(ordinal).Returns("Key2"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt64(ordinal).Returns(entity.Key2_); ordinal++; dataReader.GetName(ordinal).Returns("Value"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Value_); ordinal++; var notMappedColumnOrdinal = ordinal; dataReader.GetName(notMappedColumnOrdinal).Returns("NotMapped"); - dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(String)); + dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(string)); ordinal++; dataReader.GetName(ordinal).Returns("RowVersion"); - dataReader.GetFieldType(ordinal).Returns(typeof(Byte[])); + dataReader.GetFieldType(ordinal).Returns(typeof(byte[])); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetValue(ordinal).Returns(entity.RowVersion_); @@ -473,29 +466,21 @@ public void Materializer_Mapping_Attributes_ShouldUseAttributesMapping() _ = dataReader.DidNotReceive().IsDBNull(notMappedColumnOrdinal); _ = dataReader.DidNotReceive().GetString(notMappedColumnOrdinal); - materializedEntity.Computed_ - .Should().Be(entity.Computed_); + materializedEntity.Computed_.Should().Be(entity.Computed_); - materializedEntity.ConcurrencyToken_ - .Should().BeEquivalentTo(entity.ConcurrencyToken_); + materializedEntity.ConcurrencyToken_.Should().BeEquivalentTo(entity.ConcurrencyToken_); - materializedEntity.Identity_ - .Should().Be(entity.Identity_); + materializedEntity.Identity_.Should().Be(entity.Identity_); - materializedEntity.Key1_ - .Should().Be(entity.Key1_); + materializedEntity.Key1_.Should().Be(entity.Key1_); - materializedEntity.Key2_ - .Should().Be(entity.Key2_); + materializedEntity.Key2_.Should().Be(entity.Key2_); - materializedEntity.Value_ - .Should().Be(entity.Value_); + materializedEntity.Value_.Should().Be(entity.Value_); - materializedEntity.NotMapped - .Should().BeNull(); + materializedEntity.NotMapped.Should().BeNull(); - materializedEntity.RowVersion_ - .Should().BeEquivalentTo(entity.RowVersion_); + materializedEntity.RowVersion_.Should().BeEquivalentTo(entity.RowVersion_); } [Fact] @@ -511,48 +496,48 @@ public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() var ordinal = 0; dataReader.GetName(ordinal).Returns("Computed"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Computed_); ordinal++; dataReader.GetName(ordinal).Returns("ConcurrencyToken"); - dataReader.GetFieldType(ordinal).Returns(typeof(Byte[])); + dataReader.GetFieldType(ordinal).Returns(typeof(byte[])); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetValue(ordinal).Returns(entity.ConcurrencyToken_); ordinal++; dataReader.GetName(ordinal).Returns("Identity"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Identity_); ordinal++; dataReader.GetName(ordinal).Returns("Key1"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt64(ordinal).Returns(entity.Key1_); ordinal++; dataReader.GetName(ordinal).Returns("Key2"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt64(ordinal).Returns(entity.Key2_); ordinal++; dataReader.GetName(ordinal).Returns("Value"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Value_); ordinal++; var notMappedColumnOrdinal = ordinal; dataReader.GetName(notMappedColumnOrdinal).Returns("NotMapped"); - dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(String)); + dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(string)); ordinal++; dataReader.GetName(ordinal).Returns("RowVersion"); - dataReader.GetFieldType(ordinal).Returns(typeof(Byte[])); + dataReader.GetFieldType(ordinal).Returns(typeof(byte[])); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetValue(ordinal).Returns(entity.RowVersion_); @@ -563,29 +548,21 @@ public void Materializer_Mapping_FluentApi_ShouldUseFluentApiMapping() _ = dataReader.DidNotReceive().IsDBNull(notMappedColumnOrdinal); _ = dataReader.DidNotReceive().GetString(notMappedColumnOrdinal); - materializedEntity.Computed_ - .Should().Be(entity.Computed_); + materializedEntity.Computed_.Should().Be(entity.Computed_); - materializedEntity.ConcurrencyToken_ - .Should().BeEquivalentTo(entity.ConcurrencyToken_); + materializedEntity.ConcurrencyToken_.Should().BeEquivalentTo(entity.ConcurrencyToken_); - materializedEntity.Identity_ - .Should().Be(entity.Identity_); + materializedEntity.Identity_.Should().Be(entity.Identity_); - materializedEntity.Key1_ - .Should().Be(entity.Key1_); + materializedEntity.Key1_.Should().Be(entity.Key1_); - materializedEntity.Key2_ - .Should().Be(entity.Key2_); + materializedEntity.Key2_.Should().Be(entity.Key2_); - materializedEntity.Value_ - .Should().Be(entity.Value_); + materializedEntity.Value_.Should().Be(entity.Value_); - materializedEntity.NotMapped - .Should().BeNull(); + materializedEntity.NotMapped.Should().BeNull(); - materializedEntity.RowVersion_ - .Should().BeEquivalentTo(entity.RowVersion_); + materializedEntity.RowVersion_.Should().BeEquivalentTo(entity.RowVersion_); } [Fact] @@ -599,19 +576,19 @@ public void Materializer_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNam var ordinal = 0; dataReader.GetName(ordinal).Returns("Key1"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt64(ordinal).Returns(entity.Key1); ordinal++; dataReader.GetName(ordinal).Returns("Key2"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt64(ordinal).Returns(entity.Key2); ordinal++; dataReader.GetName(ordinal).Returns("Value"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Value); @@ -619,14 +596,11 @@ public void Materializer_Mapping_NoMapping_ShouldUseEntityTypeNameAndPropertyNam var materializedEntity = materializer(dataReader); - materializedEntity.Key1 - .Should().Be(entity.Key1); + materializedEntity.Key1.Should().Be(entity.Key1); - materializedEntity.Key2 - .Should().Be(entity.Key2); + materializedEntity.Key2.Should().Be(entity.Key2); - materializedEntity.Value - .Should().Be(entity.Value); + materializedEntity.Value.Should().Be(entity.Value); } [Fact] @@ -637,22 +611,22 @@ public void Materializer_NoCompatibleConstructor_NoParameterlessConstructor_Shou dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("NonExistent"); - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); Invoking(() => EntityMaterializerFactory.GetMaterializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + - "(Int64 NonExistent).*" + $"Could not materialize an instance of the type {typeof(EntityWithPublicConstructor)}. 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}" + + "(Int64 NonExistent).*" ); } [Fact] - public void - Materializer_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() + public void Materializer_NoCompatibleConstructor_PrivateParameterlessConstructor_ShouldUsePrivateConstructorAndProperties() { var entities = Generate.Multiple(1); @@ -660,18 +634,17 @@ public void dataReader.Read(); - var materializer = - EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer( + dataReader + ); var materializedEntity = materializer(dataReader); - materializedEntity - .Should().BeEquivalentTo(entities[0]); + materializedEntity.Should().BeEquivalentTo(entities[0]); } [Fact] - public void - Materializer_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() + public void Materializer_NoCompatibleConstructor_PublicParameterlessConstructor_ShouldUsePublicConstructorAndProperties() { var entities = Generate.Multiple(1); @@ -683,29 +656,28 @@ public void var materializedEntity = materializer(dataReader); - materializedEntity - .Should().BeEquivalentTo(entities[0]); + materializedEntity.Should().BeEquivalentTo(entities[0]); } [Fact] - public void - Materializer_NonNullableEntityProperty_DataReaderFieldContainsNull_ShouldThrow() + public void Materializer_NonNullableEntityProperty_DataReaderFieldContainsNull_ShouldThrow() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.IsDBNull(0).Returns(true); var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding " + - $"property of the type {typeof(Entity)} is non-nullable.*" + "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding " + + $"property of the type {typeof(Entity)} is non-nullable.*" ); } @@ -717,18 +689,15 @@ public void Materializer_NullableEntityProperty_DataReaderFieldContainsNull_Shou dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("NullableBooleanValue"); - dataReader.GetFieldType(0).Returns(typeof(Boolean)); + dataReader.GetFieldType(0).Returns(typeof(bool)); dataReader.IsDBNull(0).Returns(true); dataReader.GetBoolean(0).Throws(new SqlNullValueException()); - var materializer = EntityMaterializerFactory - .GetMaterializer(dataReader); + var materializer = EntityMaterializerFactory.GetMaterializer(dataReader); - var entity = Invoking(() => materializer(dataReader)) - .Should().NotThrow().Subject; + var entity = Invoking(() => materializer(dataReader)).Should().NotThrow().Subject; - entity.NullableBooleanValue - .Should().BeNull(); + entity.NullableBooleanValue.Should().BeNull(); } [Fact] @@ -745,8 +714,7 @@ public void Materializer_PropertiesWithDifferentCasing_ShouldMatchPropertiesCase var materializedEntityWithDifferentCasingProperties = materializer(dataReader); - materializedEntityWithDifferentCasingProperties - .Should().BeEquivalentTo(entityWithDifferentCasingProperties); + materializedEntityWithDifferentCasingProperties.Should().BeEquivalentTo(entityWithDifferentCasingProperties); } [Fact] @@ -760,7 +728,7 @@ public void Materializer_ShouldMaterializeDateTimeOffsetValue() var ordinal = 0; dataReader.GetName(ordinal).Returns("Id"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt64(ordinal).Returns(entity.Id); @@ -774,12 +742,11 @@ public void Materializer_ShouldMaterializeDateTimeOffsetValue() var materializedEntity = materializer(dataReader); - materializedEntity - .Should().BeEquivalentTo(entity); + materializedEntity.Should().BeEquivalentTo(entity); } [Fact] - public void ReflectionMaterializer_ShouldMaterializeTheSameEntityAsTheExpressionMaterializer() + public void ReflectionMaterializer_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() { var entities = Generate.Multiple(1); @@ -787,108 +754,111 @@ public void ReflectionMaterializer_ShouldMaterializeTheSameEntityAsTheExpression dataReader.Read(); - var expressionMaterializer = EntityMaterializerFactory.GetMaterializer(dataReader); - var reflectionMaterializer = GetReflectionMaterializer(dataReader); - - var materializedEntity = reflectionMaterializer(dataReader); - - materializedEntity - .Should().BeEquivalentTo(entities[0]); + var materializer = GetReflectionMaterializer(dataReader); - materializedEntity - .Should().BeEquivalentTo(expressionMaterializer(dataReader)); + materializer(dataReader).Should().BeEquivalentTo(entities[0]); } [Fact] - public void ReflectionMaterializer_Mapping_Attributes_ShouldUseAttributesMapping() + public void ReflectionMaterializer_CompatiblePublicConstructor_ShouldUsePublicConstructor() { - var entity = Generate.Single(); + var entities = Generate.Multiple(1); - var dataReader = Substitute.For(); + var dataReader = CreateEntityDataReader(entities); - dataReader.FieldCount.Returns(3); + dataReader.Read(); - var ordinal = 0; - dataReader.GetName(ordinal).Returns("Key1"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); - dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt64(ordinal).Returns(entity.Key1_); + var expressionMaterializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var reflectionMaterializer = GetReflectionMaterializer(dataReader); - ordinal++; - dataReader.GetName(ordinal).Returns("Value"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); - dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.Value_); + var materializedEntity = reflectionMaterializer(dataReader); - ordinal++; - var notMappedColumnOrdinal = ordinal; - dataReader.GetName(notMappedColumnOrdinal).Returns("NotMapped"); - dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(String)); + materializedEntity.Should().BeEquivalentTo(entities[0]); - var materializer = GetReflectionMaterializer(dataReader); + materializedEntity.Should().BeEquivalentTo(expressionMaterializer(dataReader)); + } - var materializedEntity = materializer(dataReader); + [Fact] + public void ReflectionMaterializer_ConstructorParameterValueCannotBeConverted_ShouldThrow() + { + var dataReader = CreateItemDataReader(); - _ = dataReader.DidNotReceive().IsDBNull(notMappedColumnOrdinal); - _ = dataReader.DidNotReceive().GetString(notMappedColumnOrdinal); + dataReader.GetString(2).Returns("NonExistent"); + + var expectedMessage = + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to the " + + $"type {typeof(TestEnum)} of the corresponding property of the type {typeof(Item)}. See inner " + + "exception for details.*"; + + var expectedInnerMessage = + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. That " + + "string does not match any of the names of the enum's members.*"; - materializedEntity.Key1_ - .Should().Be(entity.Key1_); + var reflectionMaterializer = GetReflectionMaterializer(dataReader); + var expressionMaterializer = EntityMaterializerFactory.GetMaterializer(dataReader); - materializedEntity.Value_ - .Should().Be(entity.Value_); + Invoking(() => reflectionMaterializer(dataReader)) + .Should() + .Throw() + .WithMessage(expectedMessage) + .WithInnerException() + .WithMessage(expectedInnerMessage); - materializedEntity.NotMapped - .Should().BeNull(); + Invoking(() => expressionMaterializer(dataReader)) + .Should() + .Throw() + .WithMessage(expectedMessage) + .WithInnerException() + .WithMessage(expectedInnerMessage); } [Fact] - public void ReflectionMaterializer_DataReaderFieldNameMatchesEntityPropertyCaseInsensitively_ShouldMaterialize() + public void ReflectionMaterializer_ConstructorParametersInADifferentOrderThanTheFields_ShouldMaterialize() { + var enumValue = Generate.Single(); + var name = Generate.Single(); + var id = Generate.Id(); + + // Item's constructor is (Id, Name, Enum); the result set deliberately returns the columns in another order. var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(1); + dataReader.FieldCount.Returns(3); - dataReader.GetName(0).Returns("id"); // lower-case - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetName(0).Returns("Name"); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); - dataReader.GetInt64(0).Returns(789); + dataReader.GetString(0).Returns(name); - var materializer = GetReflectionMaterializer(dataReader); + dataReader.GetName(1).Returns("Enum"); + dataReader.GetFieldType(1).Returns(typeof(int)); // Item.Enum is of type TestEnum. + dataReader.IsDBNull(1).Returns(false); + dataReader.GetInt32(1).Returns((int)enumValue); + + dataReader.GetName(2).Returns("Id"); + dataReader.GetFieldType(2).Returns(typeof(long)); + dataReader.IsDBNull(2).Returns(false); + dataReader.GetInt64(2).Returns(id); - materializer(dataReader).Id - .Should().Be(789); + var materializer = GetReflectionMaterializer(dataReader); + + materializer(dataReader).Should().Be(new Item(id, name, enumValue)); } [Fact] - public void ReflectionMaterializer_DataReaderHasCompatibleFieldTypes_ShouldConvertValues() + public void ReflectionMaterializer_DataReaderFieldNameMatchesEntityPropertyCaseInsensitively_ShouldMaterialize() { var dataReader = Substitute.For(); - var entityId = Generate.Id(); - var enumValue = Generate.Single(); - - dataReader.FieldCount.Returns(2); + dataReader.FieldCount.Returns(1); - dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(String)); // EntityWithEnumStoredAsInteger.Id is of type Int64. + dataReader.GetName(0).Returns("id"); // lower-case + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(entityId.ToString()); - - dataReader.GetName(1).Returns("Enum"); - dataReader.GetFieldType(1).Returns(typeof(Decimal)); // EntityWithEnumStoredAsInteger.Enum is of type TestEnum. - dataReader.IsDBNull(1).Returns(false); - dataReader.GetDecimal(1).Returns((Decimal)enumValue); - - var materializer = GetReflectionMaterializer(dataReader); - - var entity = materializer(dataReader); + dataReader.GetInt64(0).Returns(789); - entity.Id - .Should().Be(entityId); + var materializer = GetReflectionMaterializer(dataReader); - entity.Enum - .Should().Be(enumValue); + materializer(dataReader).Id.Should().Be(789); } [Fact] @@ -899,234 +869,216 @@ public void ReflectionMaterializer_DataReaderFieldValueCannotBeConverted_ShouldT dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("CharValue"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns("ab"); var materializer = GetReflectionMaterializer(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + - $"to the type {typeof(Char)} of the corresponding property of the type " + - $"{typeof(Entity)}. See inner exception for details.*" + "The column 'CharValue' returned by the SQL statement contains a value that could not be converted " + + $"to the type {typeof(char)} of the corresponding property of the type " + + $"{typeof(Entity)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly " + - "one character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly " + + "one character long." ); } [Fact] - public void ReflectionMaterializer_NonNullableEntityProperty_DataReaderFieldContainsNull_ShouldThrow() + public void ReflectionMaterializer_DataReaderHasCompatibleFieldTypes_ShouldConvertValues() { var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(1); - - dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(Int64)); - dataReader.IsDBNull(0).Returns(true); - - var materializer = GetReflectionMaterializer(dataReader); + var entityId = Generate.Id(); + var enumValue = Generate.Single(); - Invoking(() => materializer(dataReader)) - .Should().Throw() - .WithMessage( - "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding " + - $"property of the type {typeof(Entity)} is non-nullable.*" - ); - } + dataReader.FieldCount.Returns(2); - [Fact] - public void ReflectionMaterializer_NullableEntityProperty_DataReaderFieldContainsNull_ShouldMaterializeNull() - { - var dataReader = Substitute.For(); + dataReader.GetName(0).Returns("Id"); + dataReader.GetFieldType(0).Returns(typeof(string)); // EntityWithEnumStoredAsInteger.Id is of type Int64. + dataReader.IsDBNull(0).Returns(false); + dataReader.GetString(0).Returns(entityId.ToString()); - dataReader.FieldCount.Returns(1); + dataReader.GetName(1).Returns("Enum"); + dataReader.GetFieldType(1).Returns(typeof(decimal)); // EntityWithEnumStoredAsInteger.Enum is of type TestEnum. + dataReader.IsDBNull(1).Returns(false); + dataReader.GetDecimal(1).Returns((decimal)enumValue); - dataReader.GetName(0).Returns("NullableBooleanValue"); - dataReader.GetFieldType(0).Returns(typeof(Boolean)); - dataReader.IsDBNull(0).Returns(true); - dataReader.GetBoolean(0).Throws(new SqlNullValueException()); + var materializer = GetReflectionMaterializer(dataReader); - var materializer = GetReflectionMaterializer(dataReader); + var entity = materializer(dataReader); - var entity = Invoking(() => materializer(dataReader)) - .Should().NotThrow().Subject; + entity.Id.Should().Be(entityId); - entity.NullableBooleanValue - .Should().BeNull(); + entity.Enum.Should().Be(enumValue); } [Fact] - public void ReflectionMaterializer_ShouldMaterializeDateTimeOffsetValue() + public void ReflectionMaterializer_Mapping_Attributes_ShouldUseAttributesMapping() { - var entity = Generate.Single(); + var entity = Generate.Single(); var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(2); + dataReader.FieldCount.Returns(3); var ordinal = 0; - dataReader.GetName(ordinal).Returns("Id"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int64)); + dataReader.GetName(ordinal).Returns("Key1"); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt64(ordinal).Returns(entity.Id); + dataReader.GetInt64(ordinal).Returns(entity.Key1_); ordinal++; - dataReader.GetName(ordinal).Returns("DateTimeOffsetValue"); - dataReader.GetFieldType(ordinal).Returns(typeof(DateTimeOffset)); + dataReader.GetName(ordinal).Returns("Value"); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetValue(ordinal).Returns(entity.DateTimeOffsetValue); - - var materializer = GetReflectionMaterializer(dataReader); - - materializer(dataReader) - .Should().BeEquivalentTo(entity); - } + dataReader.GetInt32(ordinal).Returns(entity.Value_); - [Fact] - public void ReflectionMaterializer_CompatiblePublicConstructor_ShouldUsePublicConstructor() - { - var entities = Generate.Multiple(1); + ordinal++; + var notMappedColumnOrdinal = ordinal; + dataReader.GetName(notMappedColumnOrdinal).Returns("NotMapped"); + dataReader.GetFieldType(notMappedColumnOrdinal).Returns(typeof(string)); - var dataReader = CreateEntityDataReader(entities); + var materializer = GetReflectionMaterializer(dataReader); - dataReader.Read(); + var materializedEntity = materializer(dataReader); - var expressionMaterializer = EntityMaterializerFactory.GetMaterializer(dataReader); - var reflectionMaterializer = GetReflectionMaterializer(dataReader); + _ = dataReader.DidNotReceive().IsDBNull(notMappedColumnOrdinal); + _ = dataReader.DidNotReceive().GetString(notMappedColumnOrdinal); - var materializedEntity = reflectionMaterializer(dataReader); + materializedEntity.Key1_.Should().Be(entity.Key1_); - materializedEntity - .Should().BeEquivalentTo(entities[0]); + materializedEntity.Value_.Should().Be(entity.Value_); - materializedEntity - .Should().BeEquivalentTo(expressionMaterializer(dataReader)); + materializedEntity.NotMapped.Should().BeNull(); } [Fact] - public void ReflectionMaterializer_CompatiblePrivateConstructor_ShouldUsePrivateConstructor() + public void ReflectionMaterializer_NonNullableConstructorParameter_DataReaderFieldContainsNull_ShouldThrow() { - var entities = Generate.Multiple(1); + var dataReader = CreateItemDataReader(); - var dataReader = CreateEntityDataReader(entities); + dataReader.IsDBNull(0).Returns(true); - dataReader.Read(); + var expectedMessage = + "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding property " + + $"of the type {typeof(Item)} is non-nullable.*"; - var materializer = GetReflectionMaterializer(dataReader); + var reflectionMaterializer = GetReflectionMaterializer(dataReader); + var expressionMaterializer = EntityMaterializerFactory.GetMaterializer(dataReader); + + Invoking(() => reflectionMaterializer(dataReader)) + .Should() + .Throw() + .WithMessage(expectedMessage); - materializer(dataReader) - .Should().BeEquivalentTo(entities[0]); + Invoking(() => expressionMaterializer(dataReader)) + .Should() + .Throw() + .WithMessage(expectedMessage); } [Fact] - public void ReflectionMaterializer_ConstructorParametersInADifferentOrderThanTheFields_ShouldMaterialize() + public void ReflectionMaterializer_NonNullableEntityProperty_DataReaderFieldContainsNull_ShouldThrow() { - var enumValue = Generate.Single(); - var name = Generate.Single(); - var id = Generate.Id(); - - // Item's constructor is (Id, Name, Enum); the result set deliberately returns the columns in another order. var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(3); + dataReader.FieldCount.Returns(1); - dataReader.GetName(0).Returns("Name"); - dataReader.GetFieldType(0).Returns(typeof(String)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(name); + dataReader.GetName(0).Returns("Id"); + dataReader.GetFieldType(0).Returns(typeof(long)); + dataReader.IsDBNull(0).Returns(true); - dataReader.GetName(1).Returns("Enum"); - dataReader.GetFieldType(1).Returns(typeof(Int32)); // Item.Enum is of type TestEnum. - dataReader.IsDBNull(1).Returns(false); - dataReader.GetInt32(1).Returns((Int32)enumValue); + var materializer = GetReflectionMaterializer(dataReader); - dataReader.GetName(2).Returns("Id"); - dataReader.GetFieldType(2).Returns(typeof(Int64)); - dataReader.IsDBNull(2).Returns(false); - dataReader.GetInt64(2).Returns(id); + Invoking(() => materializer(dataReader)) + .Should() + .Throw() + .WithMessage( + "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding " + + $"property of the type {typeof(Entity)} is non-nullable.*" + ); + } + + [Fact] + public void ReflectionMaterializer_NullableConstructorParameter_DataReaderFieldContainsNull_ShouldPassNull() + { + var dataReader = CreateItemDataReader(); + + dataReader.IsDBNull(1).Returns(true); var materializer = GetReflectionMaterializer(dataReader); - materializer(dataReader) - .Should().Be(new Item(id, name, enumValue)); + materializer(dataReader).Name.Should().BeNull(); } [Fact] - public void ReflectionMaterializer_NonNullableConstructorParameter_DataReaderFieldContainsNull_ShouldThrow() + public void ReflectionMaterializer_NullableEntityProperty_DataReaderFieldContainsNull_ShouldMaterializeNull() { - var dataReader = CreateItemDataReader(); + var dataReader = Substitute.For(); - dataReader.IsDBNull(0).Returns(true); + dataReader.FieldCount.Returns(1); - var expectedMessage = - "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding property " + - $"of the type {typeof(Item)} is non-nullable.*"; + dataReader.GetName(0).Returns("NullableBooleanValue"); + dataReader.GetFieldType(0).Returns(typeof(bool)); + dataReader.IsDBNull(0).Returns(true); + dataReader.GetBoolean(0).Throws(new SqlNullValueException()); - var reflectionMaterializer = GetReflectionMaterializer(dataReader); - var expressionMaterializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var materializer = GetReflectionMaterializer(dataReader); - Invoking(() => reflectionMaterializer(dataReader)) - .Should().Throw() - .WithMessage(expectedMessage); + var entity = Invoking(() => materializer(dataReader)).Should().NotThrow().Subject; - Invoking(() => expressionMaterializer(dataReader)) - .Should().Throw() - .WithMessage(expectedMessage); + entity.NullableBooleanValue.Should().BeNull(); } [Fact] - public void ReflectionMaterializer_NullableConstructorParameter_DataReaderFieldContainsNull_ShouldPassNull() + public void ReflectionMaterializer_PrivateParameterlessConstructor_ShouldUsePrivateConstructor() { - var dataReader = CreateItemDataReader(); + var entities = Generate.Multiple(1); - dataReader.IsDBNull(1).Returns(true); + var dataReader = CreateEntityDataReader(entities); - var materializer = GetReflectionMaterializer(dataReader); + dataReader.Read(); - materializer(dataReader).Name - .Should().BeNull(); + var materializer = GetReflectionMaterializer(dataReader); + + materializer(dataReader).Should().BeEquivalentTo(entities[0]); } [Fact] - public void ReflectionMaterializer_ConstructorParameterValueCannotBeConverted_ShouldThrow() + public void ReflectionMaterializer_ShouldMaterializeDateTimeOffsetValue() { - var dataReader = CreateItemDataReader(); + var entity = Generate.Single(); - dataReader.GetString(2).Returns("NonExistent"); + var dataReader = Substitute.For(); - var expectedMessage = - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to the " + - $"type {typeof(TestEnum)} of the corresponding property of the type {typeof(Item)}. See inner " + - "exception for details.*"; + dataReader.FieldCount.Returns(2); - var expectedInnerMessage = - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. That " + - "string does not match any of the names of the enum's members.*"; + var ordinal = 0; + dataReader.GetName(ordinal).Returns("Id"); + dataReader.GetFieldType(ordinal).Returns(typeof(long)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt64(ordinal).Returns(entity.Id); - var reflectionMaterializer = GetReflectionMaterializer(dataReader); - var expressionMaterializer = EntityMaterializerFactory.GetMaterializer(dataReader); + ordinal++; + dataReader.GetName(ordinal).Returns("DateTimeOffsetValue"); + dataReader.GetFieldType(ordinal).Returns(typeof(DateTimeOffset)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetValue(ordinal).Returns(entity.DateTimeOffsetValue); - Invoking(() => reflectionMaterializer(dataReader)) - .Should().Throw() - .WithMessage(expectedMessage) - .WithInnerException() - .WithMessage(expectedInnerMessage); + var materializer = GetReflectionMaterializer(dataReader); - Invoking(() => expressionMaterializer(dataReader)) - .Should().Throw() - .WithMessage(expectedMessage) - .WithInnerException() - .WithMessage(expectedInnerMessage); + materializer(dataReader).Should().BeEquivalentTo(entity); } [Fact] - public void ReflectionMaterializer_PrivateParameterlessConstructor_ShouldUsePrivateConstructor() + public void ReflectionMaterializer_ShouldMaterializeTheSameEntityAsTheExpressionMaterializer() { var entities = Generate.Multiple(1); @@ -1134,10 +1086,14 @@ public void ReflectionMaterializer_PrivateParameterlessConstructor_ShouldUsePriv dataReader.Read(); - var materializer = GetReflectionMaterializer(dataReader); + var expressionMaterializer = EntityMaterializerFactory.GetMaterializer(dataReader); + var reflectionMaterializer = GetReflectionMaterializer(dataReader); + + var materializedEntity = reflectionMaterializer(dataReader); - materializer(dataReader) - .Should().BeEquivalentTo(entities[0]); + materializedEntity.Should().BeEquivalentTo(entities[0]); + + materializedEntity.Should().BeEquivalentTo(expressionMaterializer(dataReader)); } [Fact] @@ -1158,6 +1114,18 @@ public void ShouldGuardAgainstNullArguments() ); } + /// + /// Creates a multi-column reader over the mapped, readable properties of . + /// + /// The entities the reader reads. + /// The created reader. + private static EnumerableReader CreateEntityDataReader(IEnumerable entities) => + new( + entities, + [.. EntityHelper.GetEntityTypeMetadata(typeof(Entity)).MappedProperties.Where(a => a.CanRead)], + EnumerableReaderOptions.SerializeEnums | EnumerableReaderOptions.ReadCharsAsStrings + ); + /// /// Creates a data reader whose three columns match the constructor of , so that both /// materializers take the constructor-injection strategy. @@ -1170,35 +1138,23 @@ private static DbDataReader CreateItemDataReader() dataReader.FieldCount.Returns(3); dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.IsDBNull(0).Returns(false); dataReader.GetInt64(0).Returns(Generate.Id()); dataReader.GetName(1).Returns("Name"); - dataReader.GetFieldType(1).Returns(typeof(String)); + dataReader.GetFieldType(1).Returns(typeof(string)); dataReader.IsDBNull(1).Returns(false); - dataReader.GetString(1).Returns(Generate.Single()); + dataReader.GetString(1).Returns(Generate.Single()); dataReader.GetName(2).Returns("Enum"); - dataReader.GetFieldType(2).Returns(typeof(String)); // Item.Enum is of type TestEnum. + dataReader.GetFieldType(2).Returns(typeof(string)); // Item.Enum is of type TestEnum. dataReader.IsDBNull(2).Returns(false); dataReader.GetString(2).Returns(Generate.Single().ToString()); return dataReader; } - /// - /// Creates a multi-column reader over the mapped, readable properties of . - /// - /// The entities the reader reads. - /// The created reader. - private static EnumerableReader CreateEntityDataReader(IEnumerable entities) => - new( - entities, - [.. EntityHelper.GetEntityTypeMetadata(typeof(Entity)).MappedProperties.Where(a => a.CanRead)], - EnumerableReaderOptions.SerializeEnums | EnumerableReaderOptions.ReadCharsAsStrings - ); - /// /// Creates the reflection materializer - the one that serves applications published with Native AOT - for the /// shape of . diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/MaterializerFactoryHelperTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/MaterializerFactoryHelperTests.cs index 4ff45e8..999772b 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/MaterializerFactoryHelperTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/MaterializerFactoryHelperTests.cs @@ -17,11 +17,10 @@ public void CreateGetDbDataReaderFieldValueExpression_BytesFieldType_ShouldCallG Expression.Constant(1), 1, "FieldA", - typeof(Byte[]) + typeof(byte[]) ); - expression.ToString() - .Should().Match("Convert(*DbDataReader*.GetValue(1), Byte[])"); + expression.ToString().Should().Match("Convert(*DbDataReader*.GetValue(1), Byte[])"); } [Fact] @@ -37,8 +36,7 @@ public void CreateGetDbDataReaderFieldValueExpression_DateOnlyFieldType_ShouldCa typeof(DateOnly) ); - expression.ToString() - .Should().Match("Convert(*DbDataReader*.GetValue(1), DateOnly)"); + expression.ToString().Should().Match("Convert(*DbDataReader*.GetValue(1), DateOnly)"); } [Fact] @@ -54,25 +52,24 @@ public void CreateGetDbDataReaderFieldValueExpression_DateTimeOffsetFieldType_Sh typeof(DateTimeOffset) ); - expression.ToString() - .Should().Match("Convert(*DbDataReader*.GetValue(1), DateTimeOffset)"); + expression.ToString().Should().Match("Convert(*DbDataReader*.GetValue(1), DateTimeOffset)"); } [Theory] - [InlineData(typeof(Boolean), "*DbDataReader*.GetBoolean(1)")] - [InlineData(typeof(Byte), "*DbDataReader*.GetByte(1)")] + [InlineData(typeof(bool), "*DbDataReader*.GetBoolean(1)")] + [InlineData(typeof(byte), "*DbDataReader*.GetByte(1)")] [InlineData(typeof(DateTime), "*DbDataReader*.GetDateTime(1)")] - [InlineData(typeof(Decimal), "*DbDataReader*.GetDecimal(1)")] - [InlineData(typeof(Double), "*DbDataReader*.GetDouble(1)")] - [InlineData(typeof(Single), "*DbDataReader*.GetFloat(1)")] + [InlineData(typeof(decimal), "*DbDataReader*.GetDecimal(1)")] + [InlineData(typeof(double), "*DbDataReader*.GetDouble(1)")] + [InlineData(typeof(float), "*DbDataReader*.GetFloat(1)")] [InlineData(typeof(Guid), "*DbDataReader*.GetGuid(1)")] - [InlineData(typeof(Int16), "*DbDataReader*.GetInt16(1)")] - [InlineData(typeof(Int32), "*DbDataReader*.GetInt32(1)")] - [InlineData(typeof(Int64), "*DbDataReader*.GetInt64(1)")] - [InlineData(typeof(String), "*DbDataReader*.GetString(1)")] + [InlineData(typeof(short), "*DbDataReader*.GetInt16(1)")] + [InlineData(typeof(int), "*DbDataReader*.GetInt32(1)")] + [InlineData(typeof(long), "*DbDataReader*.GetInt64(1)")] + [InlineData(typeof(string), "*DbDataReader*.GetString(1)")] public void CreateGetDbDataReaderFieldValueExpression_ShouldCallTypedGetMethod( Type fieldType, - String expectedExpression + string expectedExpression ) { var dataReader = Substitute.For(); @@ -85,8 +82,7 @@ String expectedExpression fieldType ); - expression.ToString() - .Should().Match(expectedExpression); + expression.ToString().Should().Match(expectedExpression); } [Fact] @@ -102,8 +98,7 @@ public void CreateGetDbDataReaderFieldValueExpression_TimeOnlyFieldType_ShouldCa typeof(TimeOnly) ); - expression.ToString() - .Should().Match("Convert(*DbDataReader*.GetValue(1), TimeOnly)"); + expression.ToString().Should().Match("Convert(*DbDataReader*.GetValue(1), TimeOnly)"); } [Fact] @@ -119,8 +114,7 @@ public void CreateGetDbDataReaderFieldValueExpression_TimeSpanFieldType_ShouldCa typeof(TimeSpan) ); - expression.ToString() - .Should().Match("Convert(*DbDataReader*.GetValue(1), TimeSpan)"); + expression.ToString().Should().Match("Convert(*DbDataReader*.GetValue(1), TimeSpan)"); } [Fact] @@ -128,7 +122,8 @@ public void CreateGetDbDataReaderFieldValueExpression_UnsupportedFieldType_Shoul { var dataReader = Substitute.For(); - Invoking(() => MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueExpression( + Invoking(() => + MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueExpression( Expression.Constant(dataReader), Expression.Constant(1), 1, @@ -136,13 +131,15 @@ public void CreateGetDbDataReaderFieldValueExpression_UnsupportedFieldType_Shoul typeof(BigInteger) ) ) - .Should().Throw() + .Should() + .Throw() .WithMessage( - $"The data type {typeof(BigInteger)} of the column 'FieldA' returned by the SQL statement is not " + - "supported.*" + $"The data type {typeof(BigInteger)} of the column 'FieldA' returned by the SQL statement is not " + + "supported.*" ); - Invoking(() => MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueExpression( + Invoking(() => + MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueExpression( Expression.Constant(dataReader), Expression.Constant(1), 1, @@ -150,38 +147,40 @@ public void CreateGetDbDataReaderFieldValueExpression_UnsupportedFieldType_Shoul typeof(BigInteger) ) ) - .Should().Throw() + .Should() + .Throw() .WithMessage( - $"The data type {typeof(BigInteger)} of the 2nd column returned by the SQL statement is not " + - "supported.*" + $"The data type {typeof(BigInteger)} of the 2nd column returned by the SQL statement is not " + + "supported.*" ); } [Theory] - [InlineData(typeof(Boolean), nameof(DbDataReader.GetBoolean))] - [InlineData(typeof(Byte), nameof(DbDataReader.GetByte))] + [InlineData(typeof(bool), nameof(DbDataReader.GetBoolean))] + [InlineData(typeof(byte), nameof(DbDataReader.GetByte))] [InlineData(typeof(DateTime), nameof(DbDataReader.GetDateTime))] - [InlineData(typeof(Decimal), nameof(DbDataReader.GetDecimal))] - [InlineData(typeof(Double), nameof(DbDataReader.GetDouble))] - [InlineData(typeof(Single), nameof(DbDataReader.GetFloat))] + [InlineData(typeof(decimal), nameof(DbDataReader.GetDecimal))] + [InlineData(typeof(double), nameof(DbDataReader.GetDouble))] + [InlineData(typeof(float), nameof(DbDataReader.GetFloat))] [InlineData(typeof(Guid), nameof(DbDataReader.GetGuid))] - [InlineData(typeof(Int16), nameof(DbDataReader.GetInt16))] - [InlineData(typeof(Int32), nameof(DbDataReader.GetInt32))] - [InlineData(typeof(Int64), nameof(DbDataReader.GetInt64))] - [InlineData(typeof(String), nameof(DbDataReader.GetString))] - [InlineData(typeof(Byte[]), nameof(DbDataReader.GetValue))] + [InlineData(typeof(short), nameof(DbDataReader.GetInt16))] + [InlineData(typeof(int), nameof(DbDataReader.GetInt32))] + [InlineData(typeof(long), nameof(DbDataReader.GetInt64))] + [InlineData(typeof(string), nameof(DbDataReader.GetString))] + [InlineData(typeof(byte[]), nameof(DbDataReader.GetValue))] [InlineData(typeof(DateOnly), nameof(DbDataReader.GetValue))] [InlineData(typeof(DateTimeOffset), nameof(DbDataReader.GetValue))] [InlineData(typeof(TimeOnly), nameof(DbDataReader.GetValue))] [InlineData(typeof(TimeSpan), nameof(DbDataReader.GetValue))] public void CreateGetDbDataReaderFieldValueFunction_ShouldCallTheSameMethodAsTheExpression( Type fieldType, - String expectedDbDataReaderMethodName + string expectedDbDataReaderMethodName ) { var dataReader = Substitute.For(); - MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueExpression( + MaterializerFactoryHelper + .CreateGetDbDataReaderFieldValueExpression( Expression.Constant(dataReader), Expression.Constant(1), 1, @@ -189,39 +188,37 @@ String expectedDbDataReaderMethodName fieldType ) .ToString() - .Should().Contain($".{expectedDbDataReaderMethodName}(1)"); + .Should() + .Contain($".{expectedDbDataReaderMethodName}(1)"); _ = MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(1, "FieldA", fieldType)(dataReader); - dataReader.ReceivedCalls().Select(call => call.GetMethodInfo().Name) - .Should().Equal(expectedDbDataReaderMethodName); + dataReader + .ReceivedCalls() + .Select(call => call.GetMethodInfo().Name) + .Should() + .Equal(expectedDbDataReaderMethodName); } [Fact] public void CreateGetDbDataReaderFieldValueFunction_UnsupportedFieldType_ShouldThrow() { - Invoking(() => MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction( - 1, - "FieldA", - typeof(BigInteger) - ) + Invoking(() => + MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(1, "FieldA", typeof(BigInteger)) ) - .Should().Throw() + .Should() + .Throw() .WithMessage( - $"The data type {typeof(BigInteger)} of the column 'FieldA' returned by the SQL statement is not " + - "supported.*" + $"The data type {typeof(BigInteger)} of the column 'FieldA' returned by the SQL statement is not " + + "supported.*" ); - Invoking(() => MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction( - 1, - "", - typeof(BigInteger) - ) - ) - .Should().Throw() + Invoking(() => MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(1, "", typeof(BigInteger))) + .Should() + .Throw() .WithMessage( - $"The data type {typeof(BigInteger)} of the 2nd column returned by the SQL statement is not " + - "supported.*" + $"The data type {typeof(BigInteger)} of the 2nd column returned by the SQL statement is not " + + "supported.*" ); } @@ -230,14 +227,15 @@ public void DbDataReaderGetValueMethod_ShouldReferenceDbDataReaderGetValue() { var method = MaterializerFactoryHelper.DbDataReaderGetValueMethod; - method.DeclaringType - .Should().Be(typeof(DbDataReader)); + method.DeclaringType.Should().Be(typeof(DbDataReader)); - method.Name - .Should().Be(nameof(DbDataReader.GetValue)); + method.Name.Should().Be(nameof(DbDataReader.GetValue)); - method.GetParameters().Select(p => (p.Name, p.ParameterType)) - .Should().BeEquivalentTo([("ordinal", typeof(Int32))]); + method + .GetParameters() + .Select(p => (p.Name, p.ParameterType)) + .Should() + .BeEquivalentTo([("ordinal", typeof(int))]); } [Fact] @@ -245,58 +243,53 @@ public void DbDataReaderIsDBNullMethod_ShouldReferenceDbDataReaderIsDBNull() { var method = MaterializerFactoryHelper.DbDataReaderIsDBNullMethod; - method.DeclaringType - .Should().Be(typeof(DbDataReader)); + method.DeclaringType.Should().Be(typeof(DbDataReader)); - method.Name - .Should().Be(nameof(DbDataReader.IsDBNull)); + method.Name.Should().Be(nameof(DbDataReader.IsDBNull)); - method.GetParameters().Select(p => (p.Name, p.ParameterType)) - .Should().BeEquivalentTo([("ordinal", typeof(Int32))]); + method + .GetParameters() + .Select(p => (p.Name, p.ParameterType)) + .Should() + .BeEquivalentTo([("ordinal", typeof(int))]); } [Theory] - [InlineData(typeof(Boolean), true)] - [InlineData(typeof(Byte), true)] + [InlineData(typeof(bool), true)] + [InlineData(typeof(byte), true)] [InlineData(typeof(DateOnly), true)] [InlineData(typeof(DateTime), true)] - [InlineData(typeof(Decimal), true)] - [InlineData(typeof(Double), true)] - [InlineData(typeof(Single), true)] + [InlineData(typeof(decimal), true)] + [InlineData(typeof(double), true)] + [InlineData(typeof(float), true)] [InlineData(typeof(Guid), true)] - [InlineData(typeof(Int16), true)] - [InlineData(typeof(Int32), true)] - [InlineData(typeof(Int64), true)] - [InlineData(typeof(String), true)] - [InlineData(typeof(Byte[]), true)] + [InlineData(typeof(short), true)] + [InlineData(typeof(int), true)] + [InlineData(typeof(long), true)] + [InlineData(typeof(string), true)] + [InlineData(typeof(byte[]), true)] [InlineData(typeof(TimeSpan), true)] [InlineData(typeof(TimeOnly), true)] [InlineData(typeof(DateTimeOffset), true)] - [InlineData(typeof(Char), false)] + [InlineData(typeof(char), false)] [InlineData(typeof(BigInteger), false)] public void IsDbDataReaderTypedGetMethodAvailable_ShouldReturnWhetherTypedGetMethodIsAvailable( Type fieldType, - Boolean expectedResult - ) => - MaterializerFactoryHelper.IsDbDataReaderTypedGetMethodAvailable(fieldType) - .Should().Be(expectedResult); + bool expectedResult + ) => MaterializerFactoryHelper.IsDbDataReaderTypedGetMethodAvailable(fieldType).Should().Be(expectedResult); [Fact] public void MakeValueConverterConvertValueToTypeMethod_ShouldReferenceValueConverterConvertValueToType() { - var method = MaterializerFactoryHelper.MakeValueConverterConvertValueToTypeMethod(typeof(Int32)); + var method = MaterializerFactoryHelper.MakeValueConverterConvertValueToTypeMethod(typeof(int)); - method.DeclaringType - .Should().Be(typeof(ValueConverter)); + method.DeclaringType.Should().Be(typeof(ValueConverter)); - method.Name - .Should().Be(nameof(ValueConverter.ConvertValueToType)); + method.Name.Should().Be(nameof(ValueConverter.ConvertValueToType)); - method.GetGenericArguments() - .Should().Equal(typeof(Int32)); + method.GetGenericArguments().Should().Equal(typeof(int)); - method.GetParameters().Select(p => (p.Name, p.ParameterType)) - .Should().Equal(("value", typeof(Object))); + method.GetParameters().Select(p => (p.Name, p.ParameterType)).Should().Equal(("value", typeof(object))); } [Fact] @@ -310,22 +303,16 @@ public void ShouldGuardAgainstNullArguments() Expression.Constant(1), 1, "FieldA", - typeof(Int32) + typeof(int) ) ); ArgumentNullGuardVerifier.Verify(() => - MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction( - 1, - "FieldA", - typeof(Int32) - ) + MaterializerFactoryHelper.CreateGetDbDataReaderFieldValueFunction(1, "FieldA", typeof(int)) ); ArgumentNullGuardVerifier.Verify(() => - MaterializerFactoryHelper.IsDbDataReaderTypedGetMethodAvailable( - typeof(Int32) - ) + MaterializerFactoryHelper.IsDbDataReaderTypedGetMethodAvailable(typeof(int)) ); } @@ -334,14 +321,11 @@ public void StringCharsProperty_ShouldReferenceStringCharsIndexer() { var property = MaterializerFactoryHelper.StringCharsProperty; - property.DeclaringType - .Should().Be(typeof(String)); + property.DeclaringType.Should().Be(typeof(string)); - property.Name - .Should().Be("Chars"); + property.Name.Should().Be("Chars"); - property.PropertyType - .Should().Be(typeof(Char)); + property.PropertyType.Should().Be(typeof(char)); } [Fact] @@ -349,17 +333,17 @@ public void StringConcatMethod_ShouldReferenceStringConcatWithThreeStringParamet { var method = MaterializerFactoryHelper.StringConcatMethod; - method.DeclaringType - .Should().Be(typeof(String)); + method.DeclaringType.Should().Be(typeof(string)); - method.Name - .Should().Be(nameof(String.Concat)); + method.Name.Should().Be(nameof(string.Concat)); - method.GetParameters().Select(p => (p.Name, p.ParameterType)) - .Should().Equal(("str0", typeof(String)), ("str1", typeof(String)), ("str2", typeof(String))); + method + .GetParameters() + .Select(p => (p.Name, p.ParameterType)) + .Should() + .Equal(("str0", typeof(string)), ("str1", typeof(string)), ("str2", typeof(string))); - method.ReturnType - .Should().Be(typeof(String)); + method.ReturnType.Should().Be(typeof(string)); } [Fact] @@ -367,13 +351,10 @@ public void StringLengthProperty_ShouldReferenceStringLengthProperty() { var property = MaterializerFactoryHelper.StringLengthProperty; - property.DeclaringType - .Should().Be(typeof(String)); + property.DeclaringType.Should().Be(typeof(string)); - property.Name - .Should().Be(nameof(String.Length)); + property.Name.Should().Be(nameof(string.Length)); - property.PropertyType - .Should().Be(typeof(Int32)); + property.PropertyType.Should().Be(typeof(int)); } } diff --git a/tests/DbConnectionPlus.UnitTests/Materializers/ValueTupleMaterializerFactoryTests.cs b/tests/DbConnectionPlus.UnitTests/Materializers/ValueTupleMaterializerFactoryTests.cs index e146a91..1664d9b 100644 --- a/tests/DbConnectionPlus.UnitTests/Materializers/ValueTupleMaterializerFactoryTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Materializers/ValueTupleMaterializerFactoryTests.cs @@ -16,18 +16,18 @@ public void GetMaterializer_DataReaderFieldCountDoesNotMatchValueTupleFieldCount dataReader.FieldCount.Returns(2); - Invoking(() => ValueTupleMaterializerFactory.GetMaterializer<(Int32, Int32, Int32)>(dataReader)) - .Should().Throw() + Invoking(() => ValueTupleMaterializerFactory.GetMaterializer<(int, int, int)>(dataReader)) + .Should() + .Throw() .WithMessage( - $"The SQL statement returned 2 columns, but the value tuple type {typeof((Int32, Int32, Int32))} has " + - "3 fields. 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 2 columns, but the value tuple type {typeof((int, int, int))} has " + + "3 fields. Make sure that the SQL statement returns the same number of columns as the number of " + + "fields in the value tuple type.*" ); } [Fact] - public void - GetMaterializer_DataReaderFieldTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() + public void GetMaterializer_DataReaderFieldTypeNotCompatibleWithValueTupleFieldType_ShouldThrow() { var dataReader = Substitute.For(); @@ -37,11 +37,12 @@ public void dataReader.GetFieldType(0).Returns(typeof(Guid)); Invoking(() => ValueTupleMaterializerFactory.GetMaterializer>(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - $"The data type {typeof(Guid)} of the column 'DateTime' returned by the SQL statement is not " + - $"compatible with the field type {typeof(DateTime)} of the corresponding field of the value tuple " + - $"type {typeof(ValueTuple)}.*" + $"The data type {typeof(Guid)} of the column 'DateTime' returned by the SQL statement is not " + + $"compatible with the field type {typeof(DateTime)} of the corresponding field of the value tuple " + + $"type {typeof(ValueTuple)}.*" ); dataReader.FieldCount.Returns(1); @@ -50,11 +51,12 @@ public void dataReader.GetFieldType(0).Returns(typeof(Guid)); Invoking(() => ValueTupleMaterializerFactory.GetMaterializer>(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - $"The data type {typeof(Guid)} of the 1st column returned by the SQL statement is not " + - $"compatible with the field type {typeof(DateTime)} of the corresponding field of the value tuple " + - $"type {typeof(ValueTuple)}.*" + $"The data type {typeof(Guid)} of the 1st column returned by the SQL statement is not " + + $"compatible with the field type {typeof(DateTime)} of the corresponding field of the value tuple " + + $"type {typeof(ValueTuple)}.*" ); } @@ -65,8 +67,9 @@ public void GetMaterializer_DataReaderHasNoFields_ShouldThrow() dataReader.FieldCount.Returns(0); - Invoking(() => ValueTupleMaterializerFactory.GetMaterializer>(dataReader)) - .Should().Throw() + Invoking(() => ValueTupleMaterializerFactory.GetMaterializer>(dataReader)) + .Should() + .Throw() .WithMessage("The SQL statement did not return any columns.*"); } @@ -81,10 +84,11 @@ public void GetMaterializer_DataReaderHasUnsupportedFieldType_ShouldThrow() dataReader.GetFieldType(0).Returns(typeof(BigInteger)); Invoking(() => ValueTupleMaterializerFactory.GetMaterializer>(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - $"The data type {typeof(BigInteger)} of the column 'Value' returned by the SQL statement is not " + - "supported.*" + $"The data type {typeof(BigInteger)} of the column 'Value' returned by the SQL statement is not " + + "supported.*" ); dataReader.FieldCount.Returns(1); @@ -93,7 +97,8 @@ public void GetMaterializer_DataReaderHasUnsupportedFieldType_ShouldThrow() dataReader.GetFieldType(0).Returns(typeof(BigInteger)); Invoking(() => ValueTupleMaterializerFactory.GetMaterializer>(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( $"The data type {typeof(BigInteger)} of the 1st column returned by the SQL statement is not supported.*" ); @@ -105,10 +110,9 @@ public void GetMaterializer_TypeIsNotAValueTupleType_ShouldThrow() var dataReader = Substitute.For(); Invoking(() => ValueTupleMaterializerFactory.GetMaterializer(dataReader)) - .Should().Throw() - .WithMessage( - $"The specified type {typeof(NotAValueTuple)} is not a {typeof(ValueTuple)} type.*" - ); + .Should() + .Throw() + .WithMessage($"The specified type {typeof(NotAValueTuple)} is not a {typeof(ValueTuple)} type.*"); } [Fact] @@ -122,46 +126,22 @@ public void Materializer_DataReaderHasCompatibleFieldTypes_ShouldConvertValues() dataReader.FieldCount.Returns(2); dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns(entityId.ToString()); dataReader.GetName(1).Returns("Enum"); - dataReader.GetFieldType(1).Returns(typeof(Decimal)); + dataReader.GetFieldType(1).Returns(typeof(decimal)); dataReader.IsDBNull(1).Returns(false); - dataReader.GetDecimal(1).Returns((Decimal)enumValue); + dataReader.GetDecimal(1).Returns((decimal)enumValue); - var materializer = ValueTupleMaterializerFactory.GetMaterializer<(Int64 Id, TestEnum Enum)>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer<(long Id, TestEnum Enum)>(dataReader); var entity = materializer(dataReader); - entity.Id - .Should().Be(entityId); + entity.Id.Should().Be(entityId); - entity.Enum - .Should().Be(enumValue); - } - - [Fact] - public void Materializer_EnumValueTupleField_DataReaderContainsInteger_ShouldConvertToEnumMember() - { - var dataReader = Substitute.For(); - - var enumValue = Generate.Single(); - - dataReader.FieldCount.Returns(1); - - dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(Int32)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetInt32(0).Returns((Int32)enumValue); - - var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); - - var valueTuple = materializer(dataReader); - - valueTuple.Item1 - .Should().Be(enumValue); + entity.Enum.Should().Be(enumValue); } [Fact] @@ -172,29 +152,29 @@ public void Materializer_EnumValueTupleField_DataReaderContainsIntegerNotMatchin dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(Int32)); + dataReader.GetFieldType(0).Returns(typeof(int)); dataReader.IsDBNull(0).Returns(false); dataReader.GetInt32(0).Returns(999); - var materializer = ValueTupleMaterializerFactory - .GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); } [Fact] - public void Materializer_EnumValueTupleField_DataReaderContainsString_ShouldConvertToEnumMember() + public void Materializer_EnumValueTupleField_DataReaderContainsInteger_ShouldConvertToEnumMember() { var dataReader = Substitute.For(); @@ -203,17 +183,15 @@ public void Materializer_EnumValueTupleField_DataReaderContainsString_ShouldConv dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(int)); dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(enumValue.ToString()); + dataReader.GetInt32(0).Returns((int)enumValue); - var materializer = ValueTupleMaterializerFactory - .GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); var valueTuple = materializer(dataReader); - valueTuple.Item1 - .Should().Be(enumValue); + valueTuple.Item1.Should().Be(enumValue); } [Fact] @@ -224,27 +202,48 @@ public void Materializer_EnumValueTupleField_DataReaderContainsStringNotMatching dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns("NonExistent"); - var materializer = ValueTupleMaterializerFactory - .GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. That " + - "string does not match any of the names of the enum's members.*" + $"Could not convert the string 'NonExistent' to an enum member of the type {typeof(TestEnum)}. That " + + "string does not match any of the names of the enum's members.*" ); } + [Fact] + public void Materializer_EnumValueTupleField_DataReaderContainsString_ShouldConvertToEnumMember() + { + var dataReader = Substitute.For(); + + var enumValue = Generate.Single(); + + dataReader.FieldCount.Returns(1); + + dataReader.GetName(0).Returns("Enum"); + dataReader.GetFieldType(0).Returns(typeof(string)); + dataReader.IsDBNull(0).Returns(false); + dataReader.GetString(0).Returns(enumValue.ToString()); + + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + + var valueTuple = materializer(dataReader); + + valueTuple.Item1.Should().Be(enumValue); + } + [Fact] public void Materializer_MoreThan7FieldsValueTupleType_ShouldMaterializeNestedValueTuples() { @@ -255,131 +254,126 @@ public void Materializer_MoreThan7FieldsValueTupleType_ShouldMaterializeNestedVa for (var i = 0; i < 15; i++) { dataReader.GetName(i).Returns($"Value{i + 1}"); - dataReader.GetFieldType(i).Returns(typeof(Int32)); + dataReader.GetFieldType(i).Returns(typeof(int)); dataReader.IsDBNull(i).Returns(false); dataReader.GetInt32(i).Returns(i + 1); } - var materializer = ValueTupleMaterializerFactory - .GetMaterializer<( - Int32, Int32, Int32, Int32, Int32, Int32, Int32, - Int32, Int32, Int32, Int32, Int32, Int32, Int32, - Int32 - )>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer<( + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int + )>(dataReader); var valueTuple = materializer(dataReader); - valueTuple.Item1 - .Should().Be(1); + valueTuple.Item1.Should().Be(1); - valueTuple.Item2 - .Should().Be(2); + valueTuple.Item2.Should().Be(2); - valueTuple.Item3 - .Should().Be(3); + valueTuple.Item3.Should().Be(3); - valueTuple.Item4 - .Should().Be(4); + valueTuple.Item4.Should().Be(4); - valueTuple.Item5 - .Should().Be(5); + valueTuple.Item5.Should().Be(5); - valueTuple.Item6 - .Should().Be(6); + valueTuple.Item6.Should().Be(6); - valueTuple.Item7 - .Should().Be(7); + valueTuple.Item7.Should().Be(7); - valueTuple.Rest.Item1 - .Should().Be(8); + valueTuple.Rest.Item1.Should().Be(8); - valueTuple.Rest.Item2 - .Should().Be(9); + valueTuple.Rest.Item2.Should().Be(9); - valueTuple.Rest.Item3 - .Should().Be(10); + valueTuple.Rest.Item3.Should().Be(10); - valueTuple.Rest.Item4 - .Should().Be(11); + valueTuple.Rest.Item4.Should().Be(11); - valueTuple.Rest.Item5 - .Should().Be(12); + valueTuple.Rest.Item5.Should().Be(12); - valueTuple.Rest.Item6 - .Should().Be(13); + valueTuple.Rest.Item6.Should().Be(13); - valueTuple.Rest.Item7 - .Should().Be(14); + valueTuple.Rest.Item7.Should().Be(14); - valueTuple.Rest.Rest.Item1 - .Should().Be(15); + valueTuple.Rest.Rest.Item1.Should().Be(15); } [Fact] - public void - Materializer_NonNullableCharValueTupleField_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() + public void Materializer_NonNullableCharValueTupleField_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Char"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(String.Empty); + dataReader.GetString(0).Returns(string.Empty); - var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Char' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char)}. The string must be exactly one " + - "character long." + $"Could not convert the string '' to the type {typeof(char)}. The string must be exactly one " + + "character long." ); dataReader.GetString(0).Returns("ab"); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(Char)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Char' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(char)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char)}. The string must be exactly one " + - "character long." + $"Could not convert the string 'ab' to the type {typeof(char)}. The string must be exactly one " + + "character long." ); } [Fact] - public void - Materializer_NonNullableCharValueTupleField_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() + public void Materializer_NonNullableCharValueTupleField_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); - var character = Generate.Single(); + var character = Generate.Single(); dataReader.GetName(0).Returns("Char"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns(character.ToString()); - var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); var valueTuple = materializer(dataReader); - valueTuple.Item1 - .Should().Be(character); + valueTuple.Item1.Should().Be(character); } [Fact] @@ -390,84 +384,84 @@ public void Materializer_NonNullableValueTupleField_DataReaderFieldContainsNull_ dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.IsDBNull(0).Returns(true); - var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding field " + - $"of the value tuple type {typeof(ValueTuple)} is non-nullable.*" + "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding field " + + $"of the value tuple type {typeof(ValueTuple)} is non-nullable.*" ); } [Fact] - public void - Materializer_NullableCharValueTupleField_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() + public void Materializer_NullableCharValueTupleField_DataReaderFieldContainsStringWithLengthNotOne_ShouldThrow() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Char"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(String.Empty); + dataReader.GetString(0).Returns(string.Empty); - var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(Char?)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Char' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(char?)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string '' to the type {typeof(Char?)}. The string must be exactly one " + - "character long." + $"Could not convert the string '' to the type {typeof(char?)}. The string must be exactly one " + + "character long." ); dataReader.GetString(0).Returns("ab"); Invoking(() => materializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Char' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(Char?)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" + "The column 'Char' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(char?)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" ) .WithInnerException() .WithMessage( - $"Could not convert the string 'ab' to the type {typeof(Char?)}. The string must be exactly " + - "one character long." + $"Could not convert the string 'ab' to the type {typeof(char?)}. The string must be exactly " + + "one character long." ); } [Fact] - public void - Materializer_NullableCharValueTupleField_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() + public void Materializer_NullableCharValueTupleField_DataReaderFieldContainsStringWithLengthOne_ShouldGetFirstCharacter() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); - var character = Generate.Single(); + var character = Generate.Single(); dataReader.GetName(0).Returns("Char"); - dataReader.GetFieldType(0).Returns(typeof(String)); + dataReader.GetFieldType(0).Returns(typeof(string)); dataReader.IsDBNull(0).Returns(false); dataReader.GetString(0).Returns(character.ToString()); - var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); var valueTuple = materializer(dataReader); - valueTuple.Item1 - .Should().Be(character); + valueTuple.Item1.Should().Be(character); } [Fact] @@ -477,18 +471,16 @@ public void Materializer_NullableValueTupleField_DataReaderFieldContainsNull_Sho dataReader.FieldCount.Returns(1); - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.GetName(0).Returns("Id"); dataReader.IsDBNull(0).Returns(true); dataReader.GetInt64(0).Throws(new SqlNullValueException()); - var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); - var valueTuple = Invoking(() => materializer(dataReader)) - .Should().NotThrow().Subject; + var valueTuple = Invoking(() => materializer(dataReader)).Should().NotThrow().Subject; - valueTuple.Item1 - .Should().BeNull(); + valueTuple.Item1.Should().BeNull(); } [Fact] @@ -503,13 +495,13 @@ public void Materializer_ShouldMaterialize() var ordinal = 0; dataReader.GetName(ordinal).Returns("Boolean"); - dataReader.GetFieldType(ordinal).Returns(typeof(Boolean)); + dataReader.GetFieldType(ordinal).Returns(typeof(bool)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetBoolean(ordinal).Returns(entity.BooleanValue); ordinal++; dataReader.GetName(ordinal).Returns("Char"); - dataReader.GetFieldType(ordinal).Returns(typeof(String)); + dataReader.GetFieldType(ordinal).Returns(typeof(string)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetString(ordinal).Returns(entity.CharValue.ToString()); @@ -521,12 +513,12 @@ public void Materializer_ShouldMaterialize() ordinal++; dataReader.GetName(ordinal).Returns("Nullable"); - dataReader.GetFieldType(ordinal).Returns(typeof(Decimal)); + dataReader.GetFieldType(ordinal).Returns(typeof(decimal)); dataReader.IsDBNull(ordinal).Returns(true); ordinal++; dataReader.GetName(ordinal).Returns("Enum"); - dataReader.GetFieldType(ordinal).Returns(typeof(String)); + dataReader.GetFieldType(ordinal).Returns(typeof(string)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetString(ordinal).Returns(entity.EnumValue.ToString()); @@ -538,35 +530,35 @@ public void Materializer_ShouldMaterialize() ordinal++; dataReader.GetName(ordinal).Returns("Int32"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); dataReader.IsDBNull(ordinal).Returns(false); dataReader.GetInt32(ordinal).Returns(entity.Int32Value); - var materializer = ValueTupleMaterializerFactory - .GetMaterializer<(Boolean, Char, DateTime, Decimal?, TestEnum, Guid, Int32)>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer<( + bool, + char, + DateTime, + decimal?, + TestEnum, + Guid, + int + )>(dataReader); var valueTuple = materializer(dataReader); - valueTuple.Item1 - .Should().Be(entity.BooleanValue); + valueTuple.Item1.Should().Be(entity.BooleanValue); - valueTuple.Item2 - .Should().Be(entity.CharValue); + valueTuple.Item2.Should().Be(entity.CharValue); - valueTuple.Item3 - .Should().Be(entity.DateTimeValue); + valueTuple.Item3.Should().Be(entity.DateTimeValue); - valueTuple.Item4 - .Should().BeNull(); + valueTuple.Item4.Should().BeNull(); - valueTuple.Item5 - .Should().Be(entity.EnumValue); + valueTuple.Item5.Should().Be(entity.EnumValue); - valueTuple.Item6 - .Should().Be(entity.GuidValue); + valueTuple.Item6.Should().Be(entity.GuidValue); - valueTuple.Item7 - .Should().Be(entity.Int32Value); + valueTuple.Item7.Should().Be(entity.Int32Value); } [Fact] @@ -576,19 +568,18 @@ public void Materializer_ShouldMaterializeBinaryData() dataReader.FieldCount.Returns(1); - var bytes = Generate.Single(); + var bytes = Generate.Single(); dataReader.GetName(0).Returns("Data"); - dataReader.GetFieldType(0).Returns(typeof(Byte[])); + dataReader.GetFieldType(0).Returns(typeof(byte[])); dataReader.IsDBNull(0).Returns(false); dataReader.GetValue(0).Returns(bytes); - var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); var valueTuple = materializer(dataReader); - valueTuple.Item1 - .Should().BeEquivalentTo(bytes); + valueTuple.Item1.Should().BeEquivalentTo(bytes); } [Fact] @@ -601,134 +592,120 @@ public void Materializer_ShouldSupportSingleFieldValueTupleType() dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Boolean"); - dataReader.GetFieldType(0).Returns(typeof(Boolean)); + dataReader.GetFieldType(0).Returns(typeof(bool)); dataReader.IsDBNull(0).Returns(false); dataReader.GetBoolean(0).Returns(entity.BooleanValue); - var materializer = ValueTupleMaterializerFactory - .GetMaterializer>(dataReader); + var materializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); var valueTuple = materializer(dataReader); - valueTuple.Item1 - .Should().Be(entity.BooleanValue); + valueTuple.Item1.Should().Be(entity.BooleanValue); } [Fact] - public void ReflectionMaterializer_ShouldMaterializeTheSameValueTupleAsTheExpressionMaterializer() + public void ReflectionMaterializer_DataReaderFieldHasNoName_ShouldReportThePositionOfTheField() { - var entity = Generate.Single(); - var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(7); + dataReader.FieldCount.Returns(2); - var ordinal = 0; + dataReader.GetName(0).Returns(""); + dataReader.GetFieldType(0).Returns(typeof(long)); + dataReader.IsDBNull(0).Returns(false); + dataReader.GetInt64(0).Returns(Generate.Id()); - dataReader.GetName(ordinal).Returns("Boolean"); - dataReader.GetFieldType(ordinal).Returns(typeof(Boolean)); - dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetBoolean(ordinal).Returns(entity.BooleanValue); + dataReader.GetName(1).Returns(""); + dataReader.GetFieldType(1).Returns(typeof(long)); + dataReader.IsDBNull(1).Returns(true); - ordinal++; - dataReader.GetName(ordinal).Returns("Char"); - dataReader.GetFieldType(ordinal).Returns(typeof(String)); - dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetString(ordinal).Returns(entity.CharValue.ToString()); + var expressionMaterializer = ValueTupleMaterializerFactory.GetMaterializer<(long, long)>(dataReader); + var reflectionMaterializer = GetReflectionMaterializer<(long, long)>(dataReader); - ordinal++; - dataReader.GetName(ordinal).Returns("DateTime"); - dataReader.GetFieldType(ordinal).Returns(typeof(DateTime)); - dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetDateTime(ordinal).Returns(entity.DateTimeValue); - - ordinal++; - dataReader.GetName(ordinal).Returns("Nullable"); - dataReader.GetFieldType(ordinal).Returns(typeof(Decimal)); - dataReader.IsDBNull(ordinal).Returns(true); + var expectedMessage = Invoking(() => expressionMaterializer(dataReader)) + .Should() + .Throw() + .Which.Message; - ordinal++; - dataReader.GetName(ordinal).Returns("Enum"); - dataReader.GetFieldType(ordinal).Returns(typeof(String)); - dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetString(ordinal).Returns(entity.EnumValue.ToString()); + Invoking(() => reflectionMaterializer(dataReader)) + .Should() + .Throw() + .WithMessage( + "The 2nd column returned by the SQL statement contains a NULL value, but the corresponding field " + + $"of the value tuple type {typeof((long, long))} is non-nullable." + ) + .And.Message.Should() + .Be(expectedMessage); + } - ordinal++; - dataReader.GetName(ordinal).Returns("Guid"); - dataReader.GetFieldType(ordinal).Returns(typeof(Guid)); - dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetGuid(ordinal).Returns(entity.GuidValue); + [Fact] + public void ReflectionMaterializer_DataReaderFieldValueCannotBeConverted_ShouldThrow() + { + var dataReader = Substitute.For(); - ordinal++; - dataReader.GetName(ordinal).Returns("Int32"); - dataReader.GetFieldType(ordinal).Returns(typeof(Int32)); - dataReader.IsDBNull(ordinal).Returns(false); - dataReader.GetInt32(ordinal).Returns(entity.Int32Value); + dataReader.FieldCount.Returns(1); - var expressionMaterializer = ValueTupleMaterializerFactory - .GetMaterializer<(Boolean, Char, DateTime, Decimal?, TestEnum, Guid, Int32)>(dataReader); + dataReader.GetName(0).Returns("Enum"); + dataReader.GetFieldType(0).Returns(typeof(int)); + dataReader.IsDBNull(0).Returns(false); + dataReader.GetInt32(0).Returns(999); - var reflectionMaterializer = - GetReflectionMaterializer<(Boolean, Char, DateTime, Decimal?, TestEnum, Guid, Int32)>(dataReader); + var expressionMaterializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var reflectionMaterializer = GetReflectionMaterializer>(dataReader); - var valueTuple = reflectionMaterializer(dataReader); + var expectedMessage = Invoking(() => expressionMaterializer(dataReader)) + .Should() + .Throw() + .Which.Message; - valueTuple - .Should().Be( - ( - entity.BooleanValue, - entity.CharValue, - entity.DateTimeValue, - (Decimal?)null, - entity.EnumValue, - entity.GuidValue, - entity.Int32Value - ) + Invoking(() => reflectionMaterializer(dataReader)) + .Should() + .Throw() + .WithMessage( + "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + + $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + + $"{typeof(ValueTuple)}. See inner exception for details.*" + ) + .WithInnerException() + .WithMessage( + $"Could not convert the value '999' ({typeof(int)}) to an enum member of the type " + + $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" ); - valueTuple - .Should().Be(expressionMaterializer(dataReader)); + Invoking(() => reflectionMaterializer(dataReader)) + .Should() + .Throw() + .Which.Message.Should() + .Be(expectedMessage); } [Fact] - public void ReflectionMaterializer_MoreThan7FieldsValueTupleType_ShouldMaterializeNestedValueTuples() + public void ReflectionMaterializer_DataReaderHasCompatibleFieldTypes_ShouldConvertValues() { var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(15); + var entityId = Generate.Id(); + var enumValue = Generate.Single(); - for (var i = 0; i < 15; i++) - { - dataReader.GetName(i).Returns($"Value{i + 1}"); - dataReader.GetFieldType(i).Returns(typeof(Int32)); - dataReader.IsDBNull(i).Returns(false); - dataReader.GetInt32(i).Returns(i + 1); - } + dataReader.FieldCount.Returns(2); - var expressionMaterializer = ValueTupleMaterializerFactory - .GetMaterializer<( - Int32, Int32, Int32, Int32, Int32, Int32, Int32, - Int32, Int32, Int32, Int32, Int32, Int32, Int32, - Int32 - )>(dataReader); + dataReader.GetName(0).Returns("Id"); + dataReader.GetFieldType(0).Returns(typeof(string)); + dataReader.IsDBNull(0).Returns(false); + dataReader.GetString(0).Returns(entityId.ToString()); - var reflectionMaterializer = GetReflectionMaterializer<( - Int32, Int32, Int32, Int32, Int32, Int32, Int32, - Int32, Int32, Int32, Int32, Int32, Int32, Int32, - Int32 - )>(dataReader); + dataReader.GetName(1).Returns("Enum"); + dataReader.GetFieldType(1).Returns(typeof(decimal)); + dataReader.IsDBNull(1).Returns(false); + dataReader.GetDecimal(1).Returns((decimal)enumValue); - var valueTuple = reflectionMaterializer(dataReader); + var materializer = GetReflectionMaterializer<(long Id, TestEnum Enum)>(dataReader); - valueTuple - .Should().Be((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)); + var valueTuple = materializer(dataReader); - valueTuple - .Should().Be(expressionMaterializer(dataReader)); + valueTuple.Id.Should().Be(entityId); - // The innermost value tuple is the one that only carries the 15th field. - valueTuple.Rest.Rest.Item1 - .Should().Be(15); + valueTuple.Enum.Should().Be(enumValue); } [Fact] @@ -741,67 +718,75 @@ public void ReflectionMaterializer_EightFieldsValueTupleType_ShouldMaterializeNe for (var i = 0; i < 8; i++) { dataReader.GetName(i).Returns($"Value{i + 1}"); - dataReader.GetFieldType(i).Returns(typeof(Int32)); + dataReader.GetFieldType(i).Returns(typeof(int)); dataReader.IsDBNull(i).Returns(false); dataReader.GetInt32(i).Returns(i + 1); } - var materializer = - GetReflectionMaterializer<(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32)>(dataReader); + var materializer = GetReflectionMaterializer<(int, int, int, int, int, int, int, int)>(dataReader); - materializer(dataReader) - .Should().Be((1, 2, 3, 4, 5, 6, 7, 8)); + materializer(dataReader).Should().Be((1, 2, 3, 4, 5, 6, 7, 8)); } [Fact] - public void ReflectionMaterializer_DataReaderHasCompatibleFieldTypes_ShouldConvertValues() + public void ReflectionMaterializer_MoreThan7FieldsValueTupleType_ShouldMaterializeNestedValueTuples() { var dataReader = Substitute.For(); - var entityId = Generate.Id(); - var enumValue = Generate.Single(); - - dataReader.FieldCount.Returns(2); - - dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(String)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetString(0).Returns(entityId.ToString()); - - dataReader.GetName(1).Returns("Enum"); - dataReader.GetFieldType(1).Returns(typeof(Decimal)); - dataReader.IsDBNull(1).Returns(false); - dataReader.GetDecimal(1).Returns((Decimal)enumValue); - - var materializer = GetReflectionMaterializer<(Int64 Id, TestEnum Enum)>(dataReader); - - var valueTuple = materializer(dataReader); - - valueTuple.Id - .Should().Be(entityId); + dataReader.FieldCount.Returns(15); - valueTuple.Enum - .Should().Be(enumValue); - } + for (var i = 0; i < 15; i++) + { + dataReader.GetName(i).Returns($"Value{i + 1}"); + dataReader.GetFieldType(i).Returns(typeof(int)); + dataReader.IsDBNull(i).Returns(false); + dataReader.GetInt32(i).Returns(i + 1); + } - [Fact] - public void ReflectionMaterializer_ShouldMaterializeBinaryData() - { - var dataReader = Substitute.For(); + var expressionMaterializer = ValueTupleMaterializerFactory.GetMaterializer<( + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int + )>(dataReader); - dataReader.FieldCount.Returns(1); + var reflectionMaterializer = GetReflectionMaterializer<( + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int, + int + )>(dataReader); - var bytes = Generate.Single(); + var valueTuple = reflectionMaterializer(dataReader); - dataReader.GetName(0).Returns("Data"); - dataReader.GetFieldType(0).Returns(typeof(Byte[])); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetValue(0).Returns(bytes); + valueTuple.Should().Be((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)); - var materializer = GetReflectionMaterializer>(dataReader); + valueTuple.Should().Be(expressionMaterializer(dataReader)); - materializer(dataReader).Item1 - .Should().BeEquivalentTo(bytes); + // The innermost value tuple is the one that only carries the 15th field. + valueTuple.Rest.Rest.Item1.Should().Be(15); } [Fact] @@ -812,23 +797,26 @@ public void ReflectionMaterializer_NonNullableValueTupleField_DataReaderFieldCon dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Id"); - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.IsDBNull(0).Returns(true); - var expressionMaterializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); - var reflectionMaterializer = GetReflectionMaterializer>(dataReader); + var expressionMaterializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); + var reflectionMaterializer = GetReflectionMaterializer>(dataReader); var expectedMessage = Invoking(() => expressionMaterializer(dataReader)) - .Should().Throw().Which.Message; + .Should() + .Throw() + .Which.Message; Invoking(() => reflectionMaterializer(dataReader)) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding field " + - $"of the value tuple type {typeof(ValueTuple)} is non-nullable." + "The column 'Id' returned by the SQL statement contains a NULL value, but the corresponding field " + + $"of the value tuple type {typeof(ValueTuple)} is non-nullable." ) - .And.Message - .Should().Be(expectedMessage); + .And.Message.Should() + .Be(expectedMessage); } [Fact] @@ -838,101 +826,134 @@ public void ReflectionMaterializer_NullableValueTupleField_DataReaderFieldContai dataReader.FieldCount.Returns(1); - dataReader.GetFieldType(0).Returns(typeof(Int64)); + dataReader.GetFieldType(0).Returns(typeof(long)); dataReader.GetName(0).Returns("Id"); dataReader.IsDBNull(0).Returns(true); dataReader.GetInt64(0).Throws(new SqlNullValueException()); - var materializer = GetReflectionMaterializer>(dataReader); + var materializer = GetReflectionMaterializer>(dataReader); - Invoking(() => materializer(dataReader)) - .Should().NotThrow().Subject.Item1 - .Should().BeNull(); + Invoking(() => materializer(dataReader)).Should().NotThrow().Subject.Item1.Should().BeNull(); } [Fact] - public void ReflectionMaterializer_DataReaderFieldValueCannotBeConverted_ShouldThrow() + public void ReflectionMaterializer_ShouldMaterializeBinaryData() { var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); - dataReader.GetName(0).Returns("Enum"); - dataReader.GetFieldType(0).Returns(typeof(Int32)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetInt32(0).Returns(999); - - var expressionMaterializer = ValueTupleMaterializerFactory.GetMaterializer>(dataReader); - var reflectionMaterializer = GetReflectionMaterializer>(dataReader); + var bytes = Generate.Single(); - var expectedMessage = Invoking(() => expressionMaterializer(dataReader)) - .Should().Throw().Which.Message; + dataReader.GetName(0).Returns("Data"); + dataReader.GetFieldType(0).Returns(typeof(byte[])); + dataReader.IsDBNull(0).Returns(false); + dataReader.GetValue(0).Returns(bytes); - Invoking(() => reflectionMaterializer(dataReader)) - .Should().Throw() - .WithMessage( - "The column 'Enum' returned by the SQL statement contains a value that could not be converted to " + - $"the type {typeof(TestEnum)} of the corresponding field of the value tuple type " + - $"{typeof(ValueTuple)}. See inner exception for details.*" - ) - .WithInnerException() - .WithMessage( - $"Could not convert the value '999' ({typeof(Int32)}) to an enum member of the type " + - $"{typeof(TestEnum)}. That value does not match any of the values of the enum's members.*" - ); + var materializer = GetReflectionMaterializer>(dataReader); - Invoking(() => reflectionMaterializer(dataReader)) - .Should().Throw().Which.Message - .Should().Be(expectedMessage); + materializer(dataReader).Item1.Should().BeEquivalentTo(bytes); } [Fact] - public void ReflectionMaterializer_DataReaderFieldHasNoName_ShouldReportThePositionOfTheField() + public void ReflectionMaterializer_ShouldMaterializeTheSameValueTupleAsTheExpressionMaterializer() { + var entity = Generate.Single(); + var dataReader = Substitute.For(); - dataReader.FieldCount.Returns(2); + dataReader.FieldCount.Returns(7); - dataReader.GetName(0).Returns(""); - dataReader.GetFieldType(0).Returns(typeof(Int64)); - dataReader.IsDBNull(0).Returns(false); - dataReader.GetInt64(0).Returns(Generate.Id()); + var ordinal = 0; - dataReader.GetName(1).Returns(""); - dataReader.GetFieldType(1).Returns(typeof(Int64)); - dataReader.IsDBNull(1).Returns(true); + dataReader.GetName(ordinal).Returns("Boolean"); + dataReader.GetFieldType(ordinal).Returns(typeof(bool)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetBoolean(ordinal).Returns(entity.BooleanValue); - var expressionMaterializer = ValueTupleMaterializerFactory.GetMaterializer<(Int64, Int64)>(dataReader); - var reflectionMaterializer = GetReflectionMaterializer<(Int64, Int64)>(dataReader); + ordinal++; + dataReader.GetName(ordinal).Returns("Char"); + dataReader.GetFieldType(ordinal).Returns(typeof(string)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetString(ordinal).Returns(entity.CharValue.ToString()); - var expectedMessage = Invoking(() => expressionMaterializer(dataReader)) - .Should().Throw().Which.Message; + ordinal++; + dataReader.GetName(ordinal).Returns("DateTime"); + dataReader.GetFieldType(ordinal).Returns(typeof(DateTime)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetDateTime(ordinal).Returns(entity.DateTimeValue); - Invoking(() => reflectionMaterializer(dataReader)) - .Should().Throw() - .WithMessage( - "The 2nd column returned by the SQL statement contains a NULL value, but the corresponding field " + - $"of the value tuple type {typeof((Int64, Int64))} is non-nullable." - ) - .And.Message - .Should().Be(expectedMessage); + ordinal++; + dataReader.GetName(ordinal).Returns("Nullable"); + dataReader.GetFieldType(ordinal).Returns(typeof(decimal)); + dataReader.IsDBNull(ordinal).Returns(true); + + ordinal++; + dataReader.GetName(ordinal).Returns("Enum"); + dataReader.GetFieldType(ordinal).Returns(typeof(string)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetString(ordinal).Returns(entity.EnumValue.ToString()); + + ordinal++; + dataReader.GetName(ordinal).Returns("Guid"); + dataReader.GetFieldType(ordinal).Returns(typeof(Guid)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetGuid(ordinal).Returns(entity.GuidValue); + + ordinal++; + dataReader.GetName(ordinal).Returns("Int32"); + dataReader.GetFieldType(ordinal).Returns(typeof(int)); + dataReader.IsDBNull(ordinal).Returns(false); + dataReader.GetInt32(ordinal).Returns(entity.Int32Value); + + var expressionMaterializer = ValueTupleMaterializerFactory.GetMaterializer<( + bool, + char, + DateTime, + decimal?, + TestEnum, + Guid, + int + )>(dataReader); + + var reflectionMaterializer = GetReflectionMaterializer<(bool, char, DateTime, decimal?, TestEnum, Guid, int)>( + dataReader + ); + + var valueTuple = reflectionMaterializer(dataReader); + + valueTuple + .Should() + .Be( + ( + entity.BooleanValue, + entity.CharValue, + entity.DateTimeValue, + (decimal?)null, + entity.EnumValue, + entity.GuidValue, + entity.Int32Value + ) + ); + + valueTuple.Should().Be(expressionMaterializer(dataReader)); } [Fact] public void ShouldGuardAgainstNullArguments() { ArgumentNullGuardVerifier.Verify(() => - ValueTupleMaterializerFactory.GetMaterializer>(Substitute.For()) + ValueTupleMaterializerFactory.GetMaterializer>(Substitute.For()) ); var dataReader = Substitute.For(); dataReader.FieldCount.Returns(1); dataReader.GetName(0).Returns("Value"); - dataReader.GetFieldType(0).Returns(typeof(Int32)); + dataReader.GetFieldType(0).Returns(typeof(int)); ArgumentNullGuardVerifier.Verify(() => - ValueTupleMaterializerFactory.CreateReflectionMaterializer>( + ValueTupleMaterializerFactory.CreateReflectionMaterializer>( dataReader, dataReader.GetFieldNames(), dataReader.GetFieldTypes() diff --git a/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs b/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs index b8a1117..72da3f1 100644 --- a/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs +++ b/tests/DbConnectionPlus.UnitTests/Mocks/MockDbParameterCollection.cs @@ -5,14 +5,16 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Mocks; /// public class MockDbParameterCollection : DbParameterCollection { + private readonly List parameters = []; + /// - public override Int32 Count => this.parameters.Count; + public override int Count => this.parameters.Count; /// - public override Object SyncRoot => ((ICollection)this.parameters).SyncRoot; + public override object SyncRoot => ((ICollection)this.parameters).SyncRoot; /// - public override Int32 Add(Object value) + public override int Add(object value) { this.parameters.Add((DbParameter)value); return this.Count - 1; @@ -25,23 +27,22 @@ public override Int32 Add(Object value) public override void Clear() => this.parameters.Clear(); /// - public override Boolean Contains(Object value) => this.parameters.Contains(value); + public override bool Contains(object value) => this.parameters.Contains(value); /// - public override Boolean Contains(String value) => this.IndexOf(value) != -1; + public override bool Contains(string value) => this.IndexOf(value) != -1; /// - public override void CopyTo(Array array, Int32 index) => - this.parameters.CopyTo((DbParameter[])array, index); + public override void CopyTo(Array array, int index) => this.parameters.CopyTo((DbParameter[])array, index); /// public override IEnumerator GetEnumerator() => this.parameters.GetEnumerator(); /// - public override Int32 IndexOf(Object value) => this.parameters.IndexOf((DbParameter)value); + public override int IndexOf(object value) => this.parameters.IndexOf((DbParameter)value); /// - public override Int32 IndexOf(String parameterName) + public override int IndexOf(string parameterName) { for (var index = 0; index < this.parameters.Count; ++index) { @@ -55,39 +56,34 @@ public override Int32 IndexOf(String parameterName) } /// - public override void Insert(Int32 index, Object value) => - this.parameters.Insert(index, (DbParameter)value); + public override void Insert(int index, object value) => this.parameters.Insert(index, (DbParameter)value); /// - public override void Remove(Object value) => this.parameters.Remove((DbParameter)value); + public override void Remove(object value) => this.parameters.Remove((DbParameter)value); /// - public override void RemoveAt(Int32 index) => this.parameters.RemoveAt(index); + public override void RemoveAt(int index) => this.parameters.RemoveAt(index); /// - public override void RemoveAt(String parameterName) => - this.RemoveAt(this.IndexOfChecked(parameterName)); + public override void RemoveAt(string parameterName) => this.RemoveAt(this.IndexOfChecked(parameterName)); /// - protected override DbParameter GetParameter(Int32 index) => this.parameters[index]; + protected override DbParameter GetParameter(int index) => this.parameters[index]; /// - protected override DbParameter GetParameter(String parameterName) => + protected override DbParameter GetParameter(string parameterName) => this.GetParameter(this.IndexOfChecked(parameterName)); /// - protected override void SetParameter(Int32 index, DbParameter value) => - this.parameters[index] = value; + protected override void SetParameter(int index, DbParameter value) => this.parameters[index] = value; /// - protected override void SetParameter(String parameterName, DbParameter value) => + protected override void SetParameter(string parameterName, DbParameter value) => this.SetParameter(this.IndexOfChecked(parameterName), value); - private Int32 IndexOfChecked(String parameterName) + private int IndexOfChecked(string parameterName) { var index = this.IndexOf(parameterName); return index != -1 ? index : throw new IndexOutOfRangeException(); } - - private readonly List parameters = []; } diff --git a/tests/DbConnectionPlus.UnitTests/Readers/CommandDisposingDataReaderDecoratorTests.cs b/tests/DbConnectionPlus.UnitTests/Readers/CommandDisposingDataReaderDecoratorTests.cs index 0ed80bc..5de8818 100644 --- a/tests/DbConnectionPlus.UnitTests/Readers/CommandDisposingDataReaderDecoratorTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Readers/CommandDisposingDataReaderDecoratorTests.cs @@ -11,6 +11,11 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Readers; public class CommandDisposingDataReaderDecoratorTests : UnitTestsBase { + private readonly DbCommandDisposer commandDisposer; + + private readonly DbDataReader decoratedReader; + private readonly CommandDisposingDataReaderDecorator decorator; + /// public CommandDisposingDataReaderDecoratorTests() { @@ -29,72 +34,65 @@ public CommandDisposingDataReaderDecoratorTests() } [Fact] - public void Dispose_ShouldDisposeCommandDisposer() + public async Task DisposeAsync_ShouldDisposeCommandDisposer() { - this.decorator.Dispose(); + await this.decorator.DisposeAsync(); - this.commandDisposer.Received().Dispose(); + await this.commandDisposer.Received().DisposeAsync(); } [Fact] - public async Task DisposeAsync_ShouldDisposeCommandDisposer() + public void Dispose_ShouldDisposeCommandDisposer() { - await this.decorator.DisposeAsync(); + this.decorator.Dispose(); - await this.commandDisposer.Received().DisposeAsync(); + this.commandDisposer.Received().Dispose(); } [Fact] - public void GetFieldValue_ShouldForwardToDecoratedReader() + public async Task GetFieldValueAsync_ShouldForwardToDecoratedReader() { var ordinal = Generate.SmallNumber(); var returnValue = Generate.SmallNumber(); - this.decoratedReader.GetFieldValue(ordinal).Returns(returnValue); + this.decoratedReader.GetFieldValueAsync(ordinal, CancellationToken.None) + .Returns(Task.FromResult(returnValue)); - this.decorator.GetFieldValue(ordinal) - .Should().Be(returnValue); + (await this.decorator.GetFieldValueAsync(ordinal, CancellationToken.None)).Should().Be(returnValue); - this.decoratedReader.Received().GetFieldValue(ordinal); + await this.decoratedReader.Received().GetFieldValueAsync(ordinal, CancellationToken.None); } [Fact] - public async Task GetFieldValueAsync_ShouldForwardToDecoratedReader() + public void GetFieldValue_ShouldForwardToDecoratedReader() { var ordinal = Generate.SmallNumber(); var returnValue = Generate.SmallNumber(); - this.decoratedReader.GetFieldValueAsync(ordinal, CancellationToken.None) - .Returns(Task.FromResult(returnValue)); + this.decoratedReader.GetFieldValue(ordinal).Returns(returnValue); - (await this.decorator.GetFieldValueAsync(ordinal, CancellationToken.None)) - .Should().Be(returnValue); + this.decorator.GetFieldValue(ordinal).Should().Be(returnValue); - await this.decoratedReader.Received().GetFieldValueAsync(ordinal, CancellationToken.None); + this.decoratedReader.Received().GetFieldValue(ordinal); } [Fact] public void ShouldForwardAllMethodCallsToDecoratedReader() { - var exceptions = new HashSet + var exceptions = new HashSet { nameof(CommandDisposingDataReaderDecorator.Dispose), nameof(CommandDisposingDataReaderDecorator.DisposeAsync), nameof(CommandDisposingDataReaderDecorator.GetData), nameof(CommandDisposingDataReaderDecorator.GetFieldValue), - nameof(CommandDisposingDataReaderDecorator.GetFieldValueAsync) + nameof(CommandDisposingDataReaderDecorator.GetFieldValueAsync), }; var fixture = new Fixture(); fixture.Customize(new AutoNSubstituteCustomization()); fixture.Register(() => new DataTable()); - DecoratorAssertions.AssertDecoratorForwardsAllCalls( - fixture, - this.decorator, - this.decoratedReader, - exceptions - ); + DecoratorAssertions.AssertDecoratorForwardsAllCalls(fixture, this.decorator, this.decoratedReader, exceptions); } [Fact] @@ -107,9 +105,4 @@ public void ShouldGuardAgainstNullArguments() => CancellationToken.None ) ); - - private readonly DbCommandDisposer commandDisposer; - - private readonly DbDataReader decoratedReader; - private readonly CommandDisposingDataReaderDecorator decorator; } diff --git a/tests/DbConnectionPlus.UnitTests/Readers/EnumerableReaderOptionsTests.cs b/tests/DbConnectionPlus.UnitTests/Readers/EnumerableReaderOptionsTests.cs index 5e9cb1b..96943f2 100644 --- a/tests/DbConnectionPlus.UnitTests/Readers/EnumerableReaderOptionsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Readers/EnumerableReaderOptionsTests.cs @@ -13,14 +13,9 @@ public void GetFieldType_CharPropertyReadAsString_ShouldReturnString() { Entity[] entities = [new()]; - using var reader = CreateReader( - typeof(Entity), - entities, - EnumerableReaderOptions.ReadCharsAsStrings - ); + using var reader = CreateReader(typeof(Entity), entities, EnumerableReaderOptions.ReadCharsAsStrings); - reader.GetFieldType(reader.GetOrdinal("CharValue")) - .Should().Be(typeof(String)); + reader.GetFieldType(reader.GetOrdinal("CharValue")).Should().Be(typeof(string)); } [Fact] @@ -36,8 +31,7 @@ public void GetFieldType_EnumValuesSerializedAsIntegers_ShouldReturnInt32() EnumerableReaderOptions.SerializeEnums ); - reader.GetFieldType(0) - .Should().Be(typeof(Int32)); + reader.GetFieldType(0).Should().Be(typeof(int)); } [Fact] @@ -53,8 +47,7 @@ public void GetFieldType_EnumValuesSerializedAsStrings_ShouldReturnString() EnumerableReaderOptions.SerializeEnums ); - reader.GetFieldType(0) - .Should().Be(typeof(String)); + reader.GetFieldType(0).Should().Be(typeof(string)); } [Fact] @@ -70,29 +63,22 @@ public void GetInt32_EnumValuesSerialized_ShouldReturnEnumAsInt32() foreach (var entity in entities) { - reader.Read() - .Should().BeTrue(); + reader.Read().Should().BeTrue(); - reader.GetInt32(0) - .Should().Be((Int32)entity.Enum); + reader.GetInt32(0).Should().Be((int)entity.Enum); } } [Fact] public void GetString_CharPropertyReadAsString_ShouldConvertToString() { - Entity[] entities = [new() { CharValue = Generate.Single() }]; + Entity[] entities = [new() { CharValue = Generate.Single() }]; - using var reader = CreateReader( - typeof(Entity), - entities, - EnumerableReaderOptions.ReadCharsAsStrings - ); + using var reader = CreateReader(typeof(Entity), entities, EnumerableReaderOptions.ReadCharsAsStrings); reader.Read(); - reader.GetString(reader.GetOrdinal("CharValue")) - .Should().Be(entities[0].CharValue.ToString()); + reader.GetString(reader.GetOrdinal("CharValue")).Should().Be(entities[0].CharValue.ToString()); } [Fact] @@ -108,33 +94,42 @@ public void GetString_EnumValuesSerialized_ShouldReturnEnumAsString() foreach (var entity in entities) { - reader.Read() - .Should().BeTrue(); + reader.Read().Should().BeTrue(); - reader.GetString(0) - .Should().Be(entity.Enum.ToString()); + reader.GetString(0).Should().Be(entity.Enum.ToString()); } } + [Fact] + public void GetValue_NullProperty_ShouldReturnDbNull() + { + Entity[] entities = [new() { StringValue = null! }]; + + using var reader = CreateReader(typeof(Entity), entities, EnumerableReaderOptions.None); + + reader.Read(); + + var ordinal = reader.GetOrdinal("StringValue"); + + reader.GetValue(ordinal).Should().Be(DBNull.Value); + + reader.IsDBNull(ordinal).Should().BeTrue(); + } + [Fact] public void GetValues_CharPropertyReadAsString_ShouldConvertToString() { - Entity[] entities = [new() { CharValue = Generate.Single() }]; + Entity[] entities = [new() { CharValue = Generate.Single() }]; - using var reader = CreateReader( - typeof(Entity), - entities, - EnumerableReaderOptions.ReadCharsAsStrings - ); + using var reader = CreateReader(typeof(Entity), entities, EnumerableReaderOptions.ReadCharsAsStrings); reader.Read(); - var values = new Object[reader.FieldCount]; + var values = new object[reader.FieldCount]; reader.GetValues(values); - values[reader.GetOrdinal("CharValue")] - .Should().Be(entities[0].CharValue.ToString()); + values[reader.GetOrdinal("CharValue")].Should().Be(entities[0].CharValue.ToString()); } [Fact] @@ -152,16 +147,13 @@ public void GetValues_EnumValuesSerializedAsIntegers_ShouldSerializeEnumsAsInteg foreach (var entity in entities) { - reader.Read() - .Should().BeTrue(); + reader.Read().Should().BeTrue(); - var values = new Object[reader.FieldCount]; + var values = new object[reader.FieldCount]; - reader.GetValues(values) - .Should().Be(reader.FieldCount); + reader.GetValues(values).Should().Be(reader.FieldCount); - values[0] - .Should().Be((Int32)entity.Enum); + values[0].Should().Be((int)entity.Enum); } } @@ -180,58 +172,31 @@ public void GetValues_EnumValuesSerializedAsStrings_ShouldSerializeEnumsAsString foreach (var entity in entities) { - reader.Read() - .Should().BeTrue(); + reader.Read().Should().BeTrue(); - var values = new Object[reader.FieldCount]; + var values = new object[reader.FieldCount]; - reader.GetValues(values) - .Should().Be(reader.FieldCount); + reader.GetValues(values).Should().Be(reader.FieldCount); - values[0] - .Should().Be(entity.Enum.ToString()); + values[0].Should().Be(entity.Enum.ToString()); } } [Fact] public void GetValues_NoOptions_ShouldReturnRawEnumAndCharValues() { - var entity = new Entity - { - EnumValue = Generate.Single(), - CharValue = Generate.Single() - }; + var entity = new Entity { EnumValue = Generate.Single(), CharValue = Generate.Single() }; using var reader = CreateReader(typeof(Entity), new[] { entity }, EnumerableReaderOptions.None); reader.Read(); - var values = new Object[reader.FieldCount]; + var values = new object[reader.FieldCount]; reader.GetValues(values); - values[reader.GetOrdinal("EnumValue")] - .Should().Be(entity.EnumValue); - - values[reader.GetOrdinal("CharValue")] - .Should().Be(entity.CharValue); - } - - [Fact] - public void GetValue_NullProperty_ShouldReturnDbNull() - { - Entity[] entities = [new() { StringValue = null! }]; - - using var reader = CreateReader(typeof(Entity), entities, EnumerableReaderOptions.None); - - reader.Read(); - - var ordinal = reader.GetOrdinal("StringValue"); - - reader.GetValue(ordinal) - .Should().Be(DBNull.Value); + values[reader.GetOrdinal("EnumValue")].Should().Be(entity.EnumValue); - reader.IsDBNull(ordinal) - .Should().BeTrue(); + values[reader.GetOrdinal("CharValue")].Should().Be(entity.CharValue); } /// @@ -241,7 +206,11 @@ public void GetValue_NullProperty_ShouldReturnDbNull() /// The entities the reader reads. /// The behaviours the reader applies. /// The created reader. - private static EnumerableReader CreateReader(Type entityType, IEnumerable entities, EnumerableReaderOptions options) => + private static EnumerableReader CreateReader( + Type entityType, + IEnumerable entities, + EnumerableReaderOptions options + ) => new( entities, [.. EntityHelper.GetEntityTypeMetadata(entityType).MappedProperties.Where(a => a.CanRead)], diff --git a/tests/DbConnectionPlus.UnitTests/Readers/EnumerableReaderTests.cs b/tests/DbConnectionPlus.UnitTests/Readers/EnumerableReaderTests.cs index 5356233..4f12e76 100644 --- a/tests/DbConnectionPlus.UnitTests/Readers/EnumerableReaderTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Readers/EnumerableReaderTests.cs @@ -9,51 +9,54 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Readers; public class EnumerableReaderTests : UnitTestsBase { + private const string FieldName = "Value"; + + private readonly EnumerableReader enumerableReader; + private readonly int[] testValues; + /// public EnumerableReaderTests() { - this.testValues = Generate.Single(); - this.enumerableReader = new(this.testValues, typeof(Int32), FieldName); - } - - [Fact] - public void Close_ShouldCloseReader() - { - this.enumerableReader.IsClosed - .Should().BeFalse(); - - this.enumerableReader.Close(); - - this.enumerableReader.IsClosed - .Should().BeTrue(); + this.testValues = Generate.Single(); + this.enumerableReader = new(this.testValues, typeof(int), FieldName); } [Fact] - public void Close_ShouldDisposeEnumerator() + public async Task CloseAsync_ShouldDisposeEnumerator() { var enumerable = Substitute.For(); var enumerator = Substitute.For(); enumerable.GetEnumerator().Returns(enumerator); - var reader = new EnumerableReader(enumerable, typeof(Int32), FieldName); + var reader = new EnumerableReader(enumerable, typeof(int), FieldName); - reader.Close(); + await reader.CloseAsync(); ((IDisposable)enumerator).Received().Dispose(); } [Fact] - public async Task CloseAsync_ShouldDisposeEnumerator() + public void Close_ShouldCloseReader() + { + this.enumerableReader.IsClosed.Should().BeFalse(); + + this.enumerableReader.Close(); + + this.enumerableReader.IsClosed.Should().BeTrue(); + } + + [Fact] + public void Close_ShouldDisposeEnumerator() { var enumerable = Substitute.For(); var enumerator = Substitute.For(); enumerable.GetEnumerator().Returns(enumerator); - var reader = new EnumerableReader(enumerable, typeof(Int32), FieldName); + var reader = new EnumerableReader(enumerable, typeof(int), FieldName); - await reader.CloseAsync(); + reader.Close(); ((IDisposable)enumerator).Received().Dispose(); } @@ -61,115 +64,137 @@ public async Task CloseAsync_ShouldDisposeEnumerator() [Fact] public void Constructor_FieldNameEmptyOrWhitespace_ShouldThrow() { - Invoking(() => new EnumerableReader(this.testValues, typeof(Int32), String.Empty)) - .Should().Throw(); + Invoking(() => new EnumerableReader(this.testValues, typeof(int), string.Empty)) + .Should() + .Throw(); - Invoking(() => new EnumerableReader(this.testValues, typeof(Int32), " ")) - .Should().Throw(); + Invoking(() => new EnumerableReader(this.testValues, typeof(int), " ")).Should().Throw(); } [Fact] - public void Depth_ShouldAlwaysReturnZero() => - this.enumerableReader.Depth - .Should().Be(0); + public void Depth_ShouldAlwaysReturnZero() => this.enumerableReader.Depth.Should().Be(0); [Fact] - public void Dispose_ShouldDisposeEnumerator() + public async Task DisposeAsync_ShouldDisposeEnumerator() { var enumerable = Substitute.For(); var enumerator = Substitute.For(); enumerable.GetEnumerator().Returns(enumerator); - var reader = new EnumerableReader(enumerable, typeof(Int32), FieldName); + var reader = new EnumerableReader(enumerable, typeof(int), FieldName); - reader.Dispose(); + await reader.DisposeAsync(); ((IDisposable)enumerator).Received().Dispose(); } [Fact] - public async Task DisposeAsync_ShouldDisposeEnumerator() + public void Dispose_ShouldDisposeEnumerator() { var enumerable = Substitute.For(); var enumerator = Substitute.For(); enumerable.GetEnumerator().Returns(enumerator); - var reader = new EnumerableReader(enumerable, typeof(Int32), FieldName); + var reader = new EnumerableReader(enumerable, typeof(int), FieldName); - await reader.DisposeAsync(); + reader.Dispose(); ((IDisposable)enumerator).Received().Dispose(); } [Fact] - public void FieldCount_ShouldAlwaysReturnOne() => - this.enumerableReader.FieldCount - .Should().Be(1); + public void FieldCount_ShouldAlwaysReturnOne() => this.enumerableReader.FieldCount.Should().Be(1); + + [Fact] + public void Fields_MultiColumn_ShouldMatchMappedReadableProperties() + { + var properties = EntityHelper + .GetEntityTypeMetadata(typeof(Entity)) + .MappedProperties.Where(a => a.CanRead) + .ToArray(); + + using var reader = new EnumerableReader(new Entity[] { new() }, properties, EnumerableReaderOptions.None); + + reader.FieldCount.Should().Be(properties.Length); + + Enumerable + .Range(0, properties.Length) + .Select(reader.GetName) + .Should() + .Equal(properties.Select(a => a.PropertyName)); + + properties + .Select(a => reader.GetOrdinal(a.PropertyName)) + .Should() + .Equal(Enumerable.Range(0, properties.Length)); + + reader.GetOrdinal("NonExistentField").Should().Be(-1); + } [Fact] public void GetDataTypeName_InvalidOrdinal_ShouldThrow() => Invoking(() => this.enumerableReader.GetDataTypeName(1)) - .Should().Throw() + .Should() + .Throw() .WithMessage("The specified ordinal 1 is not supported. The only supported ordinal is zero.*"); [Fact] public void GetDataTypeName_ValidOrdinal_ShouldReturnNameOfValuesTypePassedToConstructor() => - this.enumerableReader.GetDataTypeName(0) - .Should().Be(nameof(Int32)); + this.enumerableReader.GetDataTypeName(0).Should().Be(nameof(Int32)); [Fact] public void GetFieldType_InvalidOrdinal_ShouldThrow() => Invoking(() => this.enumerableReader.GetFieldType(1)) - .Should().Throw() + .Should() + .Throw() .WithMessage("The specified ordinal 1 is not supported. The only supported ordinal is zero.*"); [Fact] public void GetFieldType_ValidOrdinal_ShouldReturnValuesTypePassedToConstructor() => - this.enumerableReader.GetFieldType(0) - .Should().Be(typeof(Int32)); + this.enumerableReader.GetFieldType(0).Should().Be(typeof(int)); [Fact] public void GetName_InvalidOrdinal_ShouldThrow() => Invoking(() => this.enumerableReader.GetName(1)) - .Should().Throw() + .Should() + .Throw() .WithMessage("The specified ordinal 1 is not supported. The only supported ordinal is zero.*"); [Fact] public void GetName_ValidOrdinal_ShouldReturnFieldNamePassedToConstructor() => - this.enumerableReader.GetName(0) - .Should().Be(FieldName); + this.enumerableReader.GetName(0).Should().Be(FieldName); [Fact] public void GetOrdinal_InvalidFieldName_ShouldThrow() => Invoking(() => this.enumerableReader.GetOrdinal("nonExistentField")) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The specified field name 'nonExistentField' is not supported. The only supported field name is " + - "'Value'.*" + "The specified field name 'nonExistentField' is not supported. The only supported field name is " + + "'Value'.*" ); [Fact] public void GetOrdinal_ValidFieldName_ShouldReturnOrdinal() => - this.enumerableReader.GetOrdinal(FieldName) - .Should().Be(0); + this.enumerableReader.GetOrdinal(FieldName).Should().Be(0); [Fact] public void GetTypedValue_SingleColumn_ShouldReturnCurrentValue() { - AssertSingleColumnAccessor(true, typeof(Boolean), a => a.GetBoolean(0)); - AssertSingleColumnAccessor((Byte)7, typeof(Byte), a => a.GetByte(0)); - AssertSingleColumnAccessor('R', typeof(Char), a => a.GetChar(0)); + AssertSingleColumnAccessor(true, typeof(bool), a => a.GetBoolean(0)); + AssertSingleColumnAccessor((byte)7, typeof(byte), a => a.GetByte(0)); + AssertSingleColumnAccessor('R', typeof(char), a => a.GetChar(0)); AssertSingleColumnAccessor(new DateTime(2026, 8, 20), typeof(DateTime), a => a.GetDateTime(0)); - AssertSingleColumnAccessor(12.34m, typeof(Decimal), a => a.GetDecimal(0)); - AssertSingleColumnAccessor(12.34d, typeof(Double), a => a.GetDouble(0)); - AssertSingleColumnAccessor(12.34f, typeof(Single), a => a.GetFloat(0)); + AssertSingleColumnAccessor(12.34m, typeof(decimal), a => a.GetDecimal(0)); + AssertSingleColumnAccessor(12.34d, typeof(double), a => a.GetDouble(0)); + AssertSingleColumnAccessor(12.34f, typeof(float), a => a.GetFloat(0)); AssertSingleColumnAccessor(Guid.NewGuid(), typeof(Guid), a => a.GetGuid(0)); - AssertSingleColumnAccessor((Int16)7, typeof(Int16), a => a.GetInt16(0)); - AssertSingleColumnAccessor(7, typeof(Int32), a => a.GetInt32(0)); - AssertSingleColumnAccessor(7L, typeof(Int64), a => a.GetInt64(0)); - AssertSingleColumnAccessor("value", typeof(String), a => a.GetString(0)); + AssertSingleColumnAccessor((short)7, typeof(short), a => a.GetInt16(0)); + AssertSingleColumnAccessor(7, typeof(int), a => a.GetInt32(0)); + AssertSingleColumnAccessor(7L, typeof(long), a => a.GetInt64(0)); + AssertSingleColumnAccessor("value", typeof(string), a => a.GetString(0)); } [Fact] @@ -178,7 +203,8 @@ public void GetValue_InvalidOrdinal_ShouldThrow() this.enumerableReader.Read(); Invoking(() => this.enumerableReader.GetValue(1)) - .Should().Throw() + .Should() + .Throw() .WithMessage("The specified ordinal 1 is not supported. The only supported ordinal is zero.*"); } @@ -189,8 +215,7 @@ public void GetValue_ValidOrdinal_ShouldReturnCurrentValue() { this.enumerableReader.Read(); - this.enumerableReader.GetValue(0) - .Should().Be(value); + this.enumerableReader.GetValue(0).Should().Be(value); } } @@ -200,28 +225,48 @@ public void GetValues_BufferTooSmall_ShouldThrow() this.enumerableReader.Read(); Invoking(() => this.enumerableReader.GetValues([])) - .Should().Throw() + .Should() + .Throw() .WithMessage("The specified array must have a length greater than or equal to 1.*"); } + [Fact] + public void GetValues_MultiColumnShortBuffer_ShouldFillAvailableEntries() + { + var entity = Generate.Single(); + var properties = EntityHelper + .GetEntityTypeMetadata(typeof(Entity)) + .MappedProperties.Where(a => a.CanRead) + .ToArray(); + + using var reader = new EnumerableReader(new[] { entity }, properties, EnumerableReaderOptions.None); + + reader.Read(); + + var values = new object[2]; + + reader.GetValues(values).Should().Be(values.Length); + + values.Should().Equal(properties.Take(values.Length).Select(a => a.PropertyGetter!(entity) ?? DBNull.Value)); + } + [Fact] public void GetValues_ShouldAlwaysReturnOne() { - var values = new Object[1]; + var values = new object[1]; foreach (var _ in this.testValues) { this.enumerableReader.Read(); - this.enumerableReader.GetValues(values) - .Should().Be(1); + this.enumerableReader.GetValues(values).Should().Be(1); } } [Fact] public void GetValues_ShouldFillBufferWithValue() { - var buffer = new Object[1]; + var buffer = new object[1]; foreach (var value in this.testValues) { @@ -229,55 +274,12 @@ public void GetValues_ShouldFillBufferWithValue() this.enumerableReader.GetValues(buffer); - buffer[0] - .Should().Be(value); + buffer[0].Should().Be(value); } } [Fact] - public void Fields_MultiColumn_ShouldMatchMappedReadableProperties() - { - var properties = EntityHelper.GetEntityTypeMetadata(typeof(Entity)).MappedProperties.Where(a => a.CanRead) - .ToArray(); - - using var reader = new EnumerableReader(new Entity[] { new() }, properties, EnumerableReaderOptions.None); - - reader.FieldCount - .Should().Be(properties.Length); - - Enumerable.Range(0, properties.Length).Select(reader.GetName) - .Should().Equal(properties.Select(a => a.PropertyName)); - - properties.Select(a => reader.GetOrdinal(a.PropertyName)) - .Should().Equal(Enumerable.Range(0, properties.Length)); - - reader.GetOrdinal("NonExistentField") - .Should().Be(-1); - } - - [Fact] - public void GetValues_MultiColumnShortBuffer_ShouldFillAvailableEntries() - { - var entity = Generate.Single(); - var properties = EntityHelper.GetEntityTypeMetadata(typeof(Entity)).MappedProperties.Where(a => a.CanRead) - .ToArray(); - - using var reader = new EnumerableReader(new[] { entity }, properties, EnumerableReaderOptions.None); - - reader.Read(); - - var values = new Object[2]; - - reader.GetValues(values) - .Should().Be(values.Length); - - values.Should().Equal(properties.Take(values.Length).Select(a => a.PropertyGetter!(entity) ?? DBNull.Value)); - } - - [Fact] - public void HasRows_ShouldAlwaysReturnTrue() => - this.enumerableReader.HasRows - .Should().BeTrue(); + public void HasRows_ShouldAlwaysReturnTrue() => this.enumerableReader.HasRows.Should().BeTrue(); [Fact] public void Indexer_InvalidFieldName_ShouldThrow() @@ -285,10 +287,11 @@ public void Indexer_InvalidFieldName_ShouldThrow() this.enumerableReader.Read(); Invoking(() => this.enumerableReader["NonExistentField"]) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The specified field name 'NonExistentField' is not supported. The only supported field name is " + - "'Value'.*" + "The specified field name 'NonExistentField' is not supported. The only supported field name is " + + "'Value'.*" ); } @@ -298,7 +301,8 @@ public void Indexer_InvalidOrdinal_ShouldThrow() this.enumerableReader.Read(); Invoking(() => this.enumerableReader[1]) - .Should().Throw() + .Should() + .Throw() .WithMessage("The specified ordinal 1 is not supported. The only supported ordinal is zero.*"); } @@ -309,8 +313,7 @@ public void Indexer_ValidName_ShouldReturnCurrentValue() { this.enumerableReader.Read(); - this.enumerableReader[FieldName] - .Should().Be(value); + this.enumerableReader[FieldName].Should().Be(value); } } @@ -321,48 +324,43 @@ public void Indexer_ValidOrdinal_ShouldReturnCurrentValue() { this.enumerableReader.Read(); - this.enumerableReader[0] - .Should().Be(value); + this.enumerableReader[0].Should().Be(value); } } [Fact] public void IsClosed_ShouldReturnWhetherReaderIsClosed() { - this.enumerableReader.IsClosed - .Should().BeFalse(); + this.enumerableReader.IsClosed.Should().BeFalse(); this.enumerableReader.Close(); - this.enumerableReader.IsClosed - .Should().BeTrue(); + this.enumerableReader.IsClosed.Should().BeTrue(); } [Fact] public void IsDBNull_InvalidOrdinal_ShouldThrow() => Invoking(() => this.enumerableReader.IsDBNull(1)) - .Should().Throw() + .Should() + .Throw() .WithMessage("The specified ordinal 1 is not supported. The only supported ordinal is zero.*"); [Fact] public void IsDBNull_ValidOrdinal_ShouldReturnWhetherCurrentValueIsNull() { - var valuesWithNulls = Generate.MultipleNullable(); - var readerWithNulls = new EnumerableReader(valuesWithNulls, typeof(Int32), FieldName); + var valuesWithNulls = Generate.MultipleNullable(); + var readerWithNulls = new EnumerableReader(valuesWithNulls, typeof(int), FieldName); foreach (var value in valuesWithNulls) { readerWithNulls.Read(); - readerWithNulls.IsDBNull(0) - .Should().Be(value is null); + readerWithNulls.IsDBNull(0).Should().Be(value is null); } } [Fact] - public void NextResult_ShouldAlwaysReturnFalse() => - this.enumerableReader.NextResult() - .Should().BeFalse(); + public void NextResult_ShouldAlwaysReturnFalse() => this.enumerableReader.NextResult().Should().BeFalse(); [Fact] public void Read_ReaderIsClosed_ShouldThrow() @@ -370,7 +368,8 @@ public void Read_ReaderIsClosed_ShouldThrow() this.enumerableReader.Close(); Invoking(() => this.enumerableReader.Read()) - .Should().Throw() + .Should() + .Throw() .WithMessage("Invalid attempt to call Read when reader is closed.*"); } @@ -379,40 +378,32 @@ public void Read_ShouldReturnTrueUntilAllValuesAreRead() { foreach (var _ in this.testValues) { - this.enumerableReader.Read() - .Should().BeTrue(); + this.enumerableReader.Read().Should().BeTrue(); } - this.enumerableReader.Read() - .Should().BeFalse(); + this.enumerableReader.Read().Should().BeFalse(); } [Fact] - public void RecordsAffected_ShouldAlwaysReturnMinusOne() => - this.enumerableReader.RecordsAffected - .Should().Be(-1); + public void RecordsAffected_ShouldAlwaysReturnMinusOne() => this.enumerableReader.RecordsAffected.Should().Be(-1); [Fact] public void ShouldGuardAgainstNullArguments() => - ArgumentNullGuardVerifier.Verify(() => new EnumerableReader(this.testValues, typeof(Int32), FieldName)); + ArgumentNullGuardVerifier.Verify(() => new EnumerableReader(this.testValues, typeof(int), FieldName)); private static void AssertSingleColumnAccessor( - Object value, + object value, [DynamicallyAccessedMembers( - DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties)] - Type valuesType, - Func accessor + DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicProperties + )] + Type valuesType, + Func accessor ) { using var reader = new EnumerableReader(new[] { value }, valuesType, FieldName); reader.Read(); - accessor(reader) - .Should().Be(value); + accessor(reader).Should().Be(value); } - - private readonly EnumerableReader enumerableReader; - private readonly Int32[] testValues; - private const String FieldName = "Value"; } diff --git a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs index c5467f8..912d5ce 100644 --- a/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs +++ b/tests/DbConnectionPlus.UnitTests/SqlStatements/InterpolatedSqlStatementTests.cs @@ -11,46 +11,37 @@ public void AppendFormatted_InterpolatedParameter_ShouldStoreParameter() InterpolatedSqlStatement statement = $"SELECT {Parameter(value1)}"; - statement.Fragments - .Should().HaveCount(2); + statement.Fragments.Should().HaveCount(2); - statement.Fragments[0] - .Should().Be(new Literal("SELECT ")); + statement.Fragments[0].Should().Be(new Literal("SELECT ")); - statement.Fragments[1] - .Should().Be(new InterpolatedParameter("Value1", value1)); + statement.Fragments[1].Should().Be(new InterpolatedParameter("Value1", value1)); } [Fact] public void AppendFormatted_InterpolatedParameter_ShouldSupportComplexExpressions() { - const Double baseDiscount = 0.1; + const double baseDiscount = 0.1; var entityIds = Generate.Ids(20); - InterpolatedSqlStatement statement = - $""" - SELECT {Parameter(baseDiscount * 5 / 3)}, - {Parameter(entityIds.Where(a => a > 5).ToArray()[0])} - """; + InterpolatedSqlStatement statement = $""" + SELECT {Parameter(baseDiscount * 5 / 3)}, + {Parameter(entityIds.Where(a => a > 5).ToArray()[0])} + """; - statement.Fragments - .Should().HaveCount(4); + statement.Fragments.Should().HaveCount(4); - statement.Fragments[0] - .Should().Be(new Literal("SELECT ")); + statement.Fragments[0].Should().Be(new Literal("SELECT ")); - statement.Fragments[1] - .Should().Be(new InterpolatedParameter("BaseDiscount53", baseDiscount * 5 / 3)); + statement.Fragments[1].Should().Be(new InterpolatedParameter("BaseDiscount53", baseDiscount * 5 / 3)); - statement.Fragments[2] - .Should().Be(new Literal($",{Environment.NewLine} ")); + statement.Fragments[2].Should().Be(new Literal($",{Environment.NewLine} ")); - statement.Fragments[3] - .Should().BeEquivalentTo( - new InterpolatedParameter( - "EntityIdsWhereaa5ToArray0", - entityIds.Where(a => a > 5).ToArray()[0] - ) + statement + .Fragments[3] + .Should() + .BeEquivalentTo( + new InterpolatedParameter("EntityIdsWhereaa5ToArray0", entityIds.Where(a => a > 5).ToArray()[0]) ); } @@ -60,55 +51,47 @@ public void AppendFormatted_InterpolatedTemporaryTables_ShouldStoreTemporaryTabl var entities = Generate.Multiple(); var entityIds = Generate.Ids(); - InterpolatedSqlStatement statement = - $""" - SELECT Value FROM {TemporaryTable(entityIds)} - UNION - SELECT Id FROM {TemporaryTable(entities)} - """; + InterpolatedSqlStatement statement = $""" + SELECT Value FROM {TemporaryTable(entityIds)} + UNION + SELECT Id FROM {TemporaryTable(entities)} + """; - statement.TemporaryTables - .Should().HaveCount(2); + statement.TemporaryTables.Should().HaveCount(2); var table1 = statement.TemporaryTables[0]; - table1.Name - .Should().StartWith("EntityIds_"); + table1.Name.Should().StartWith("EntityIds_"); - table1.Values - .Should().BeEquivalentTo(entityIds); + table1.Values.Should().BeEquivalentTo(entityIds); - table1.ValuesType - .Should().Be(typeof(Int64)); + table1.ValuesType.Should().Be(typeof(long)); - statement.Fragments[2] - .Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); + statement + .Fragments[2] + .Should() + .Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); var table2 = statement.TemporaryTables[1]; - table2.Name - .Should().StartWith("Entities_"); + table2.Name.Should().StartWith("Entities_"); - table2.Values - .Should().BeEquivalentTo(entities); + table2.Values.Should().BeEquivalentTo(entities); - table2.ValuesType - .Should().Be(typeof(Entity)); + table2.ValuesType.Should().Be(typeof(Entity)); - statement.Fragments - .Should().HaveCount(4); + statement.Fragments.Should().HaveCount(4); - statement.Fragments[0] - .Should().Be(new Literal("SELECT Value FROM ")); + statement.Fragments[0].Should().Be(new Literal("SELECT Value FROM ")); - statement.Fragments[1] - .Should().Be(table1); + statement.Fragments[1].Should().Be(table1); - statement.Fragments[2] - .Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); + statement + .Fragments[2] + .Should() + .Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); - statement.Fragments[3] - .Should().Be(table2); + statement.Fragments[3].Should().Be(table2); } [Fact] @@ -120,47 +103,35 @@ public void AppendFormatted_MultipleInterpolatedParameters_ShouldStoreParameters InterpolatedSqlStatement statement = $"SELECT {Parameter(value1)}, {Parameter(value2)}, {Parameter(value3)}"; - statement.Fragments - .Should().HaveCount(6); + statement.Fragments.Should().HaveCount(6); - statement.Fragments[0] - .Should().Be(new Literal("SELECT ")); + statement.Fragments[0].Should().Be(new Literal("SELECT ")); - statement.Fragments[1] - .Should().Be(new InterpolatedParameter("Value1", value1)); + statement.Fragments[1].Should().Be(new InterpolatedParameter("Value1", value1)); - statement.Fragments[2] - .Should().Be(new Literal(", ")); + statement.Fragments[2].Should().Be(new Literal(", ")); - statement.Fragments[3] - .Should().Be(new InterpolatedParameter("Value2", value2)); + statement.Fragments[3].Should().Be(new InterpolatedParameter("Value2", value2)); - statement.Fragments[4] - .Should().Be(new Literal(", ")); + statement.Fragments[4].Should().Be(new Literal(", ")); - statement.Fragments[5] - .Should().Be(new InterpolatedParameter("Value3", value3)); + statement.Fragments[5].Should().Be(new InterpolatedParameter("Value3", value3)); } [Fact] public void AppendFormatted_ShouldFormatAndStoreLiteral() { - InterpolatedSqlStatement statement = $"SELECT {123.45,10:N2}, {123.45,-10:N2}"; + InterpolatedSqlStatement statement = $"SELECT {123.45, 10:N2}, {123.45, -10:N2}"; - statement.Fragments - .Should().HaveCount(4); + statement.Fragments.Should().HaveCount(4); - statement.Fragments[0] - .Should().Be(new Literal("SELECT ")); + statement.Fragments[0].Should().Be(new Literal("SELECT ")); - statement.Fragments[1] - .Should().Be(new Literal(" 123.45")); + statement.Fragments[1].Should().Be(new Literal(" 123.45")); - statement.Fragments[2] - .Should().Be(new Literal(", ")); + statement.Fragments[2].Should().Be(new Literal(", ")); - statement.Fragments[3] - .Should().Be(new Literal("123.45 ")); + statement.Fragments[3].Should().Be(new Literal("123.45 ")); } [Fact] @@ -170,17 +141,16 @@ public void AppendLiteral_ShouldStoreLiteral() InterpolatedSqlStatement statement = $"SELECT 1"; #pragma warning restore RCS1214 // Unnecessary interpolated string - statement.Fragments - .Should().HaveCount(1); + statement.Fragments.Should().HaveCount(1); - statement.Fragments[0] - .Should().Be(new Literal("SELECT 1")); + statement.Fragments[0].Should().Be(new Literal("SELECT 1")); } [Fact] public void Constructor_Code_Parameters_DuplicateParameterName_ShouldThrow() { - Invoking(() => new InterpolatedSqlStatement( + Invoking(() => + new InterpolatedSqlStatement( "Code", new("Parameter1", "Value1"), new("Parameter1", "Value2"), @@ -188,14 +158,16 @@ public void Constructor_Code_Parameters_DuplicateParameterName_ShouldThrow() new("Parameter2", "Value4") ) ) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The specified parameters have the following duplicate parameter names: " + - "'Parameter1', 'Parameter1', 'Parameter2', 'Parameter2'. Make sure each parameter name is only used " + - "once.*" + "The specified parameters have the following duplicate parameter names: " + + "'Parameter1', 'Parameter1', 'Parameter2', 'Parameter2'. Make sure each parameter name is only used " + + "once.*" ); - Invoking(() => new InterpolatedSqlStatement( + Invoking(() => + new InterpolatedSqlStatement( "Code", new("Parameter1", "Value1"), new("PARAMETER1", "Value2"), @@ -203,11 +175,12 @@ public void Constructor_Code_Parameters_DuplicateParameterName_ShouldThrow() new("PARAMETER2", "Value4") ) ) - .Should().Throw() + .Should() + .Throw() .WithMessage( - "The specified parameters have the following duplicate parameter names: " + - "'Parameter1', 'PARAMETER1', 'Parameter2', 'PARAMETER2'. Make sure each parameter name is only used " + - "once.*" + "The specified parameters have the following duplicate parameter names: " + + "'Parameter1', 'PARAMETER1', 'Parameter2', 'PARAMETER2'. Make sure each parameter name is only used " + + "once.*" ); } @@ -216,11 +189,9 @@ public void Constructor_Code_Parameters_ShouldStoreCodeAsLiteral() { var statement = new InterpolatedSqlStatement("SELECT 1"); - statement.Fragments - .Should().HaveCount(1); + statement.Fragments.Should().HaveCount(1); - statement.Fragments[0] - .Should().Be(new Literal("SELECT 1")); + statement.Fragments[0].Should().Be(new Literal("SELECT 1")); } [Fact] @@ -237,20 +208,15 @@ public void Constructor_Code_Parameters_ShouldStoreParameters() ("Parameter3", value3) ); - statement.Fragments - .Should().HaveCount(4); + statement.Fragments.Should().HaveCount(4); - statement.Fragments[0] - .Should().Be(new Literal("SELECT @Parameter1, @Parameter2, @Parameter3")); + statement.Fragments[0].Should().Be(new Literal("SELECT @Parameter1, @Parameter2, @Parameter3")); - statement.Fragments[1] - .Should().Be(new Parameter("Parameter1", value1)); + statement.Fragments[1].Should().Be(new Parameter("Parameter1", value1)); - statement.Fragments[2] - .Should().Be(new Parameter("Parameter2", value2)); + statement.Fragments[2].Should().Be(new Parameter("Parameter2", value2)); - statement.Fragments[3] - .Should().Be(new Parameter("Parameter3", value3)); + statement.Fragments[3].Should().Be(new Parameter("Parameter3", value3)); } [Fact] @@ -258,8 +224,7 @@ public void Constructor_LiteralLength_FormattedCount_ShouldInitializeInstance() { var statement = new InterpolatedSqlStatement(100, 10); - statement.Fragments - .Should().BeEmpty(); + statement.Fragments.Should().BeEmpty(); } [Fact] @@ -271,81 +236,67 @@ public void Fragments_ShouldGetFragments() var entityIds = Generate.Ids(); var entities = Generate.Multiple(); - InterpolatedSqlStatement statement = - $""" - SELECT {Parameter(value1)} - UNION - SELECT {Parameter(value2)} - UNION - SELECT {Parameter(value3)} - UNION - SELECT Value FROM {TemporaryTable(entityIds)} - UNION - SELECT Id FROM {TemporaryTable(entities)} - """; + InterpolatedSqlStatement statement = $""" + SELECT {Parameter(value1)} + UNION + SELECT {Parameter(value2)} + UNION + SELECT {Parameter(value3)} + UNION + SELECT Value FROM {TemporaryTable(entityIds)} + UNION + SELECT Id FROM {TemporaryTable(entities)} + """; - statement.Fragments - .Should().HaveCount(10); + statement.Fragments.Should().HaveCount(10); - statement.Fragments[0] - .Should().Be(new Literal("SELECT ")); + statement.Fragments[0].Should().Be(new Literal("SELECT ")); - statement.Fragments[1] - .Should().Be(new InterpolatedParameter("Value1", value1)); + statement.Fragments[1].Should().Be(new InterpolatedParameter("Value1", value1)); - statement.Fragments[2] - .Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT ")); + statement.Fragments[2].Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT ")); - statement.Fragments[3] - .Should().Be(new InterpolatedParameter("Value2", value2)); + statement.Fragments[3].Should().Be(new InterpolatedParameter("Value2", value2)); - statement.Fragments[4] - .Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT ")); + statement.Fragments[4].Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT ")); - statement.Fragments[5] - .Should().Be(new InterpolatedParameter("Value3", value3)); + statement.Fragments[5].Should().Be(new InterpolatedParameter("Value3", value3)); - statement.Fragments[6] - .Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Value FROM ")); + statement + .Fragments[6] + .Should() + .Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Value FROM ")); - var table1 = statement.Fragments[7] - .Should().BeOfType().Subject; + var table1 = statement.Fragments[7].Should().BeOfType().Subject; - table1.Name - .Should().StartWith("EntityIds_"); + table1.Name.Should().StartWith("EntityIds_"); - table1.Values - .Should().BeEquivalentTo(entityIds); + table1.Values.Should().BeEquivalentTo(entityIds); - table1.ValuesType - .Should().Be(typeof(Int64)); + table1.ValuesType.Should().Be(typeof(long)); - statement.Fragments[8] - .Should().Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); + statement + .Fragments[8] + .Should() + .Be(new Literal($"{Environment.NewLine}UNION{Environment.NewLine}SELECT Id FROM ")); - var table2 = statement.Fragments[9] - .Should().BeOfType().Subject; + var table2 = statement.Fragments[9].Should().BeOfType().Subject; - table2.Name - .Should().StartWith("Entities_"); + table2.Name.Should().StartWith("Entities_"); - table2.Values - .Should().BeEquivalentTo(entities); + table2.Values.Should().BeEquivalentTo(entities); - table2.ValuesType - .Should().Be(typeof(Entity)); + table2.ValuesType.Should().Be(typeof(Entity)); } [Fact] public void FromString_EmptyString_ShouldCreateEmptyStatement() { - var statement = InterpolatedSqlStatement.FromString(String.Empty); + var statement = InterpolatedSqlStatement.FromString(string.Empty); - statement.Fragments - .Should().HaveCount(1); + statement.Fragments.Should().HaveCount(1); - statement.Fragments[0] - .Should().Be(new Literal(String.Empty)); + statement.Fragments[0].Should().Be(new Literal(string.Empty)); } [Fact] @@ -353,42 +304,38 @@ public void FromString_ShouldCreateSqlStatementFromString() { var statement = InterpolatedSqlStatement.FromString("SELECT 1"); - statement.Fragments - .Should().HaveCount(1); + statement.Fragments.Should().HaveCount(1); - statement.Fragments[0] - .Should().Be(new Literal("SELECT 1")); + statement.Fragments[0].Should().Be(new Literal("SELECT 1")); } [Fact] public void ImplicitConversion_NullValue_ShouldThrow() => Invoking(() => - { - const String? sql = null; + { + const string? sql = null; #pragma warning disable RCS1124 // Inline local variable - InterpolatedSqlStatement statement = sql!; + InterpolatedSqlStatement statement = sql!; #pragma warning restore RCS1124 // Inline local variable - return statement; - } - ) - .Should().Throw(); + return statement; + }) + .Should() + .Throw(); [Fact] public void ImplicitConversion_ShouldCreateSqlStatementFromString() { InterpolatedSqlStatement statement = "SELECT 1"; - statement.Fragments - .Should().HaveCount(1); + statement.Fragments.Should().HaveCount(1); - statement.Fragments[0] - .Should().Be(new Literal("SELECT 1")); + statement.Fragments[0].Should().Be(new Literal("SELECT 1")); } [Fact] public void ShouldGuardAgainstNullArguments() { - (String, Object?)[] parameters = [("Parameter1", "Value1")]; + (string, object?)[] parameters = [("Parameter1", "Value1")]; ArgumentNullGuardVerifier.Verify(() => new InterpolatedSqlStatement("SELECT 1", parameters)); ArgumentNullGuardVerifier.Verify(() => InterpolatedSqlStatement.FromString("SELECT 1")); @@ -400,27 +347,21 @@ public void TemporaryTables_ShouldGetInterpolatedTemporaryTables() var entities = Generate.Multiple(); var entityIds = Generate.Ids(); - InterpolatedSqlStatement statement = - $""" - SELECT Value FROM {TemporaryTable(entityIds)} - UNION - SELECT Id FROM {TemporaryTable(entities)} - """; + InterpolatedSqlStatement statement = $""" + SELECT Value FROM {TemporaryTable(entityIds)} + UNION + SELECT Id FROM {TemporaryTable(entities)} + """; - statement.TemporaryTables - .Should().HaveCount(2); + statement.TemporaryTables.Should().HaveCount(2); - var table1 = statement.TemporaryTables[0] - .Should().BeOfType().Subject; + var table1 = statement.TemporaryTables[0].Should().BeOfType().Subject; - table1.Name - .Should().StartWith("EntityIds_"); + table1.Name.Should().StartWith("EntityIds_"); - table1.Values - .Should().BeEquivalentTo(entityIds); + table1.Values.Should().BeEquivalentTo(entityIds); - table1.ValuesType - .Should().Be(typeof(Int64)); + table1.ValuesType.Should().Be(typeof(long)); } [Fact] @@ -430,93 +371,88 @@ public void ToString_ShouldReturnStringRepresentationOfStatement() { new(1, "A", TestEnum.Value1), new(2, "B", TestEnum.Value2), - new(3, "C", TestEnum.Value3) + new(3, "C", TestEnum.Value3), }; - List ids = [1, 2, 3]; + List ids = [1, 2, 3]; - const String name = "B"; + const string name = "B"; const TestEnum enumValue = TestEnum.Value2; InterpolatedSqlStatement statement = $""" - SELECT * - FROM {TemporaryTable(items)} TItem - WHERE TItem.Id IN ( - SELECT Value - FROM {TemporaryTable(ids)} - ) - AND - TItem.Name = {Parameter(name)} - AND - TItem.Enum = {Parameter(enumValue)} - """; + SELECT * + FROM {TemporaryTable(items)} TItem + WHERE TItem.Id IN ( + SELECT Value + FROM {TemporaryTable(ids)} + ) + AND + TItem.Name = {Parameter(name)} + AND + TItem.Enum = {Parameter(enumValue)} + """; var temporaryTables = statement.TemporaryTables; - temporaryTables - .Should().HaveCount(2); + temporaryTables.Should().HaveCount(2); var itemsTable = temporaryTables[0]; - itemsTable.Name - .Should().StartWith("Items_"); + itemsTable.Name.Should().StartWith("Items_"); - itemsTable.Values - .Should().Be(items); + itemsTable.Values.Should().Be(items); - itemsTable.ValuesType - .Should().Be(typeof(Item)); + itemsTable.ValuesType.Should().Be(typeof(Item)); var idsTable = temporaryTables[1]; - idsTable.Name - .Should().StartWith("Ids_"); + idsTable.Name.Should().StartWith("Ids_"); - idsTable.Values - .Should().Be(ids); + idsTable.Values.Should().Be(ids); - idsTable.ValuesType - .Should().Be(typeof(Int32)); + idsTable.ValuesType.Should().Be(typeof(int)); - statement.ToString() - .Should().Be( + statement + .ToString() + .Should() + .Be( $$""" - SQL Statement - - Statement Code - -------------- - SELECT * - FROM {{itemsTable.Name}} TItem - WHERE TItem.Id IN ( - SELECT Value - FROM {{idsTable.Name}} - ) - AND - TItem.Name = @Name - AND - TItem.Enum = @EnumValue - -------------- - - Statement Parameters - -------------------- - Name = 'B' (System.String) - EnumValue = 'Value2' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.TestEnum) - - Statement Temporary Tables - -------------------------- - - {{itemsTable.Name}} - -------------------------------------- - 'Item { Id = 1, Name = A, Enum = Value1 }' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.Item) - 'Item { Id = 2, Name = B, Enum = Value2 }' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.Item) - 'Item { Id = 3, Name = C, Enum = Value3 }' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.Item) - - {{idsTable.Name}} - ------------------------------------ - '1' (System.Int32) - '2' (System.Int32) - '3' (System.Int32) - - """ + SQL Statement + + Statement Code + -------------- + SELECT * + FROM {{itemsTable.Name}} TItem + WHERE TItem.Id IN ( + SELECT Value + FROM {{idsTable.Name}} + ) + AND + TItem.Name = @Name + AND + TItem.Enum = @EnumValue + -------------- + + Statement Parameters + -------------------- + Name = 'B' (System.String) + EnumValue = 'Value2' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.TestEnum) + + Statement Temporary Tables + -------------------------- + + {{itemsTable.Name}} + -------------------------------------- + 'Item { Id = 1, Name = A, Enum = Value1 }' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.Item) + 'Item { Id = 2, Name = B, Enum = Value2 }' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.Item) + 'Item { Id = 3, Name = C, Enum = Value3 }' (RentADeveloper.DbConnectionPlus.UnitTests.TestData.Item) + + {{idsTable.Name}} + ------------------------------------ + '1' (System.Int32) + '2' (System.Int32) + '3' (System.Int32) + + """ ); } } diff --git a/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs b/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs index d83d1a0..1a5995d 100644 --- a/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/StatementMethodTestsBase.cs @@ -7,23 +7,46 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests; /// /// Base class for unit tests of methods that execute SQL statements. /// -public abstract class StatementMethodTestsBase : UnitTestsBase +/// The asynchronous version of the statement method to test. +/// The synchronous version of the statement method to test. +public abstract class StatementMethodTestsBase( + Func< + DbConnection, + InterpolatedSqlStatement, + DbTransaction?, + TimeSpan?, + CommandType, + CancellationToken, + Task + > asyncTestMethod, + Action< + DbConnection, + InterpolatedSqlStatement, + DbTransaction?, + TimeSpan?, + CommandType, + CancellationToken + > syncTestMethod +) : UnitTestsBase { - /// - /// Initializes a new instance of the class. - /// - /// The asynchronous version of the statement method to test. - /// The synchronous version of the statement method to test. - protected StatementMethodTestsBase( - Func - asyncTestMethod, - Action - syncTestMethod - ) - { - this.asyncTestMethod = asyncTestMethod; - this.syncTestMethod = syncTestMethod; - } + private readonly Func< + DbConnection, + InterpolatedSqlStatement, + DbTransaction?, + TimeSpan?, + CommandType, + CancellationToken, + Task + > asyncTestMethod = asyncTestMethod; + + private readonly Action< + DbConnection, + InterpolatedSqlStatement, + DbTransaction?, + TimeSpan?, + CommandType, + CancellationToken + > syncTestMethod = syncTestMethod; [Fact] public async Task AsyncMethod_ShouldUseCommandTimeout() @@ -39,10 +62,11 @@ await this.asyncTestMethod( TestContext.Current.CancellationToken ); - this.MockInterceptDbCommand.Received().Invoke( - Arg.Is(cmd => cmd.CommandTimeout == (Int32)timeout.TotalSeconds), - Arg.Any>() - ); + this.MockInterceptDbCommand.Received() + .Invoke( + Arg.Is(cmd => cmd.CommandTimeout == (int)timeout.TotalSeconds), + Arg.Any>() + ); } [Fact] @@ -57,10 +81,11 @@ await this.asyncTestMethod( TestContext.Current.CancellationToken ); - this.MockInterceptDbCommand.Received().Invoke( - Arg.Is(cmd => cmd.CommandType == CommandType.StoredProcedure), - Arg.Any>() - ); + this.MockInterceptDbCommand.Received() + .Invoke( + Arg.Is(cmd => cmd.CommandType == CommandType.StoredProcedure), + Arg.Any>() + ); } [Fact] @@ -77,10 +102,11 @@ await this.asyncTestMethod( TestContext.Current.CancellationToken ); - this.MockInterceptDbCommand.Received().Invoke( - Arg.Is(cmd => cmd.Transaction == transaction), - Arg.Any>() - ); + this.MockInterceptDbCommand.Received() + .Invoke( + Arg.Is(cmd => cmd.Transaction == transaction), + Arg.Any>() + ); } [Fact] @@ -97,10 +123,11 @@ public void SyncMethod_ShouldUseCommandTimeout() TestContext.Current.CancellationToken ); - this.MockInterceptDbCommand.Received().Invoke( - Arg.Is(cmd => cmd.CommandTimeout == (Int32)timeout.TotalSeconds), - Arg.Any>() - ); + this.MockInterceptDbCommand.Received() + .Invoke( + Arg.Is(cmd => cmd.CommandTimeout == (int)timeout.TotalSeconds), + Arg.Any>() + ); } [Fact] @@ -115,10 +142,11 @@ public void SyncMethod_ShouldUseCommandType() TestContext.Current.CancellationToken ); - this.MockInterceptDbCommand.Received().Invoke( - Arg.Is(cmd => cmd.CommandType == CommandType.StoredProcedure), - Arg.Any>() - ); + this.MockInterceptDbCommand.Received() + .Invoke( + Arg.Is(cmd => cmd.CommandType == CommandType.StoredProcedure), + Arg.Any>() + ); } [Fact] @@ -135,17 +163,10 @@ public void SyncMethod_ShouldUseTransaction() TestContext.Current.CancellationToken ); - this.MockInterceptDbCommand.Received().Invoke( - Arg.Is(cmd => cmd.Transaction == transaction), - Arg.Any>() - ); + this.MockInterceptDbCommand.Received() + .Invoke( + Arg.Is(cmd => cmd.Transaction == transaction), + Arg.Any>() + ); } - - private readonly - Func - asyncTestMethod; - - private readonly - Action - syncTestMethod; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs index 69598e0..c94281e 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Entity.cs @@ -2,27 +2,27 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record Entity { - 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 DateOnly DateOnlyValue { 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; } public Guid GuidValue { get; set; } [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 Boolean? NullableBooleanValue { get; set; } - public Single SingleValue { get; set; } - public String StringValue { get; set; } = null!; + public bool? NullableBooleanValue { get; set; } + public float SingleValue { get; set; } + public string StringValue { get; set; } = null!; public TimeOnly TimeOnlyValue { get; set; } public TimeSpan TimeSpanValue { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDateTimeOffset.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDateTimeOffset.cs index 45714a2..a86a100 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDateTimeOffset.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDateTimeOffset.cs @@ -5,5 +5,5 @@ public record EntityWithDateTimeOffset public DateTimeOffset DateTimeOffsetValue { get; set; } [Key] - public Int64 Id { get; set; } + public long Id { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs index 1eb1226..9952410 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithDifferentCasingProperties.cs @@ -4,30 +4,30 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record EntityWithDifferentCasingProperties { - 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 DateOnly DateOnlyVALUE { 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; } public Guid GuidVALUE { get; set; } [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; } [NotMapped] - public String? NotMappedProperty { get; set; } + public string? NotMappedProperty { get; set; } - public Boolean? NullableBooleanVALUE { get; set; } - public Single SingleVALUE { get; set; } - public String StringVALUE { get; set; } = null!; + public bool? NullableBooleanVALUE { get; set; } + public float SingleVALUE { get; set; } + public string StringVALUE { get; set; } = null!; public TimeOnly TimeOnlyVALUE { get; set; } public TimeSpan TimeSpanVALUE { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumStoredAsInteger.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumStoredAsInteger.cs index d8efbd0..122270f 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumStoredAsInteger.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumStoredAsInteger.cs @@ -5,5 +5,5 @@ public record EntityWithEnumStoredAsInteger public TestEnum Enum { get; set; } [Key] - public Int64 Id { get; set; } + public long Id { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumStoredAsString.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumStoredAsString.cs index 4f760d2..0307132 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumStoredAsString.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithEnumStoredAsString.cs @@ -5,5 +5,5 @@ public record EntityWithEnumStoredAsString public TestEnum Enum { get; set; } [Key] - public Int64 Id { get; set; } + public long Id { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithMultipleIdentityProperties.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithMultipleIdentityProperties.cs index 5046068..cd5651e 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithMultipleIdentityProperties.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithMultipleIdentityProperties.cs @@ -3,8 +3,8 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class EntityWithMultipleIdentityProperties { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Int64 Identity1 { get; set; } + public long Identity1 { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Int64 Identity2 { get; set; } + public long Identity2 { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithObjectProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithObjectProperty.cs index ff75eaf..e28139d 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithObjectProperty.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithObjectProperty.cs @@ -2,5 +2,5 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class EntityWithObjectProperty { - public Object Value { get; set; } = null!; + public object Value { get; set; } = null!; } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs index 2055c8d..3ded5a3 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateConstructor.cs @@ -3,23 +3,23 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record EntityWithPrivateConstructor : Entity { private EntityWithPrivateConstructor( - Byte[] bytesValue, - Boolean booleanValue, - Byte byteValue, - Char charValue, + byte[] bytesValue, + bool booleanValue, + byte byteValue, + char charValue, DateOnly dateOnlyValue, DateTime dateTimeValue, - Decimal decimalValue, - Double doubleValue, + decimal decimalValue, + double doubleValue, TestEnum enumValue, Guid guidValue, - Int64 id, - Int16 int16Value, - Int32 int32Value, - Int64 int64Value, - Boolean? nullableBooleanValue, - Single singleValue, - String stringValue, + long id, + short int16Value, + int int32Value, + long int64Value, + bool? nullableBooleanValue, + float singleValue, + string stringValue, TimeOnly timeOnlyValue, TimeSpan timeSpanValue ) diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs index ee5febd..4525b13 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPrivateParameterlessConstructor.cs @@ -2,7 +2,5 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record EntityWithPrivateParameterlessConstructor : Entity { - private EntityWithPrivateParameterlessConstructor() - { - } + private EntityWithPrivateParameterlessConstructor() { } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs index c2efb63..7aa90ab 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithPublicConstructor.cs @@ -1,5 +1,7 @@ // ReSharper disable ConvertToPrimaryConstructor +// An explicit public constructor is the whole point of this fixture: it is what the constructor-injection +// materializer binds to. A primary constructor would change what is under test. #pragma warning disable IDE0290 namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; @@ -7,23 +9,23 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record EntityWithPublicConstructor : Entity { public EntityWithPublicConstructor( - Byte[] bytesValue, - Boolean booleanValue, - Byte byteValue, - Char charValue, + byte[] bytesValue, + bool booleanValue, + byte byteValue, + char charValue, DateOnly dateOnlyValue, DateTime dateTimeValue, - Decimal decimalValue, - Double doubleValue, + decimal decimalValue, + double doubleValue, TestEnum enumValue, Guid guidValue, - Int64 id, - Int16 int16Value, - Int32 int32Value, - Int64 int64Value, - Boolean? nullableBooleanValue, - Single singleValue, - String stringValue, + long id, + short int16Value, + int int32Value, + long int64Value, + bool? nullableBooleanValue, + float singleValue, + string stringValue, TimeOnly timeOnlyValue, TimeSpan timeSpanValue ) diff --git a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithoutKeyProperty.cs b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithoutKeyProperty.cs index 89c4136..76d2347 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/EntityWithoutKeyProperty.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/EntityWithoutKeyProperty.cs @@ -2,5 +2,5 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class EntityWithoutKeyProperty { - public Int32 Value { get; set; } + public int Value { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionA.cs b/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionA.cs index aa57321..1f0c7af 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionA.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionA.cs @@ -5,42 +5,34 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class FakeConnectionA : DbConnection { /// - [AllowNull] - public override String ConnectionString { get; set; } + public override string DataSource => null!; /// - public override String Database => - null!; + public override string Database => null!; /// - public override String DataSource => - null!; + public override string ServerVersion => null!; /// - public override String ServerVersion => - null!; + public override ConnectionState State => ConnectionState.Closed; /// - public override ConnectionState State => - ConnectionState.Closed; + [AllowNull] + public override string ConnectionString { get; set; } /// - public override void ChangeDatabase(String databaseName) => - throw new NotImplementedException(); + public override void ChangeDatabase(string databaseName) => throw new NotImplementedException(); /// - public override void Close() => - throw new NotImplementedException(); + public override void Close() => throw new NotImplementedException(); /// - public override void Open() => - throw new NotImplementedException(); + public override void Open() => throw new NotImplementedException(); /// protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw new NotImplementedException(); /// - protected override DbCommand CreateDbCommand() => - throw new NotImplementedException(); + protected override DbCommand CreateDbCommand() => throw new NotImplementedException(); } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionB.cs b/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionB.cs index 03da059..c1f6eea 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionB.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionB.cs @@ -5,42 +5,34 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class FakeConnectionB : DbConnection { /// - [AllowNull] - public override String ConnectionString { get; set; } + public override string DataSource => null!; /// - public override String Database => - null!; + public override string Database => null!; /// - public override String DataSource => - null!; + public override string ServerVersion => null!; /// - public override String ServerVersion => - null!; + public override ConnectionState State => ConnectionState.Closed; /// - public override ConnectionState State => - ConnectionState.Closed; + [AllowNull] + public override string ConnectionString { get; set; } /// - public override void ChangeDatabase(String databaseName) => - throw new NotImplementedException(); + public override void ChangeDatabase(string databaseName) => throw new NotImplementedException(); /// - public override void Close() => - throw new NotImplementedException(); + public override void Close() => throw new NotImplementedException(); /// - public override void Open() => - throw new NotImplementedException(); + public override void Open() => throw new NotImplementedException(); /// protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw new NotImplementedException(); /// - protected override DbCommand CreateDbCommand() => - throw new NotImplementedException(); + protected override DbCommand CreateDbCommand() => throw new NotImplementedException(); } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionC.cs b/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionC.cs index d2fecd9..a941f03 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionC.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/FakeConnectionC.cs @@ -5,42 +5,34 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class FakeConnectionC : FakeConnectionA { /// - [AllowNull] - public override String ConnectionString { get; set; } + public override string DataSource => null!; /// - public override String Database => - null!; + public override string Database => null!; /// - public override String DataSource => - null!; + public override string ServerVersion => null!; /// - public override String ServerVersion => - null!; + public override ConnectionState State => ConnectionState.Closed; /// - public override ConnectionState State => - ConnectionState.Closed; + [AllowNull] + public override string ConnectionString { get; set; } /// - public override void ChangeDatabase(String databaseName) => - throw new NotImplementedException(); + public override void ChangeDatabase(string databaseName) => throw new NotImplementedException(); /// - public override void Close() => - throw new NotImplementedException(); + public override void Close() => throw new NotImplementedException(); /// - public override void Open() => - throw new NotImplementedException(); + public override void Open() => throw new NotImplementedException(); /// protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw new NotImplementedException(); /// - protected override DbCommand CreateDbCommand() => - throw new NotImplementedException(); + protected override DbCommand CreateDbCommand() => throw new NotImplementedException(); } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs index 1697765..141dd4f 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Generate.cs @@ -17,6 +17,17 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; /// public static class Generate { + /// + /// The characters used for Char generation. + /// We only use alphabetic characters for Char generation to avoid issues with databases that do not support + /// certain characters. + /// + private static readonly char[] characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray(); + + private static readonly Faker faker; + private static readonly Fixture fixture; + private static long entityId = 1; + /// /// Initializes the class. /// @@ -27,83 +38,76 @@ static Generate() fixture.Customize(new OmitIgnoredPropertiesCustomization()); - fixture.Register(() => faker.Random.Bool()); - fixture.Register(() => faker.Random.Byte()); - fixture.Register(() => faker.Random.Bytes(SmallNumber())); - fixture.Register(() => characters[faker.Random.Int(0, characters.Length - 1)]); + fixture.Register(() => faker.Random.Bool()); + fixture.Register(() => faker.Random.Byte()); + fixture.Register(() => faker.Random.Bytes(SmallNumber())); + fixture.Register(() => characters[faker.Random.Int(0, characters.Length - 1)]); fixture.Register(() => faker.Date.PastDateOnly()); fixture.Register(() => - { - var dateTime = faker.Date.Past(); - - // We limit to seconds precision because not all database systems support a higher precision. - return new( - dateTime.Year, - dateTime.Month, - dateTime.Day, - dateTime.Hour, - dateTime.Minute, - dateTime.Second, - DateTimeKind.Local - ); - } - ); + { + var dateTime = faker.Date.Past(); + + // We limit to seconds precision because not all database systems support a higher precision. + return new( + dateTime.Year, + dateTime.Month, + dateTime.Day, + dateTime.Hour, + dateTime.Minute, + dateTime.Second, + DateTimeKind.Local + ); + }); fixture.Register(() => - { - var dateTimeOffset = faker.Date.PastOffset(); - - // We limit to seconds precision because not all database systems support a higher precision. - return new( - dateTimeOffset.Year, - dateTimeOffset.Month, - dateTimeOffset.Day, - dateTimeOffset.Hour, - dateTimeOffset.Minute, - dateTimeOffset.Second, - dateTimeOffset.Offset - ); - } - ); - fixture.Register(() => - { - // We limit to 10 fractional digits because not all database systems support a higher precision. - return Math.Round(faker.Random.Decimal(0, 999), 10); - } - ); - fixture.Register(() => - { - // We limit to 3 fractional digits because not all database systems support a higher precision. - return Math.Round(faker.Random.Double(0, 999), 3); - } - ); + { + var dateTimeOffset = faker.Date.PastOffset(); + + // We limit to seconds precision because not all database systems support a higher precision. + return new( + dateTimeOffset.Year, + dateTimeOffset.Month, + dateTimeOffset.Day, + dateTimeOffset.Hour, + dateTimeOffset.Minute, + dateTimeOffset.Second, + dateTimeOffset.Offset + ); + }); + fixture.Register(() => + { + // We limit to 10 fractional digits because not all database systems support a higher precision. + return Math.Round(faker.Random.Decimal(0, 999), 10); + }); + fixture.Register(() => + { + // We limit to 3 fractional digits because not all database systems support a higher precision. + return Math.Round(faker.Random.Double(0, 999), 3); + }); fixture.Register(() => faker.Random.Guid()); - fixture.Register(() => faker.Random.Short()); - fixture.Register(() => faker.Random.Int()); - fixture.Register(() => Interlocked.Increment(ref entityId)); - fixture.Register(() => - { - // We limit to 3 fractional digits because not all database systems support a higher precision. - return (Single)Math.Round(faker.Random.Float(0, 999), 3); - } - ); - fixture.Register(() => faker.Lorem.Sentence()); + fixture.Register(() => faker.Random.Short()); + fixture.Register(() => faker.Random.Int()); + fixture.Register(() => Interlocked.Increment(ref entityId)); + fixture.Register(() => + { + // We limit to 3 fractional digits because not all database systems support a higher precision. + return (float)Math.Round(faker.Random.Float(0, 999), 3); + }); + fixture.Register(() => faker.Lorem.Sentence()); fixture.Register(() => faker.Random.Enum()); fixture.Register(() => - { - var timeOnly = faker.Date.RecentTimeOnly(); + { + var timeOnly = faker.Date.RecentTimeOnly(); - // We limit to seconds precision because not all database systems support a higher precision. - return new(timeOnly.Hour, timeOnly.Minute, timeOnly.Second); - } - ); + // We limit to seconds precision because not all database systems support a higher precision. + return new(timeOnly.Hour, timeOnly.Minute, timeOnly.Second); + }); fixture.Register(() => - { - var timeSpan = faker.Date.Timespan(new TimeSpan(0, 23, 59, 59)); + { + var timeSpan = faker.Date.Timespan(new TimeSpan(0, 23, 59, 59)); - // We limit to seconds precision because not all database systems support a higher precision. - return new(timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds); - } - ); + // We limit to seconds precision because not all database systems support a higher precision. + return new(timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds); + }); #pragma warning disable S2930 var cancellationTokenSource = new CancellationTokenSource(); @@ -119,8 +123,7 @@ static Generate() /// Generates an ID. /// /// An ID. - public static Int64 Id() => - Interlocked.Increment(ref entityId); + public static long Id() => Interlocked.Increment(ref entityId); /// /// Generates the specified number of IDs. @@ -130,7 +133,7 @@ public static Int64 Id() => /// If omitted a small random number () will be used. /// /// A list of IDs. - public static List Ids(Int32? numberOfIds = null) => + public static List Ids(int? numberOfIds = null) => [.. Enumerable.Range(0, numberOfIds ?? SmallNumber()).Select(_ => Interlocked.Increment(ref entityId))]; /// @@ -141,8 +144,7 @@ public static List Ids(Int32? numberOfIds = null) => /// /// A list of objects containing the same data as . /// - public static List MapTo(IEnumerable objects) => - objects.Adapt>(); + public static List MapTo(IEnumerable objects) => objects.Adapt>(); /// /// Maps to an instance of containing the same data. @@ -152,8 +154,7 @@ public static List MapTo(IEnumerable objects) => /// /// An instance of containing the same data as . /// - public static TTarget MapTo(Object obj) => - obj.Adapt(); + public static TTarget MapTo(object obj) => obj.Adapt(); /// /// Generates a list of instances of the type populated with test data. @@ -164,7 +165,7 @@ public static TTarget MapTo(Object obj) => /// If omitted a small random number () will be used. /// /// A list of instances of the type populated with test data. - public static List Multiple(Int32? numberOfObjects = null) + public static List Multiple(int? numberOfObjects = null) { fixture.RepeatCount = numberOfObjects ?? SmallNumber(); return fixture.Create>(); @@ -183,7 +184,7 @@ public static List Multiple(Int32? numberOfObjects = null) /// A list of random values of the type and values. /// The list is guaranteed to have at least 50% of its values set to . /// - public static List MultipleNullable(Int32? numberOfValues = null) + public static List MultipleNullable(int? numberOfValues = null) where T : struct { fixture.RepeatCount = numberOfValues ?? SmallNumber(); @@ -212,24 +213,24 @@ public static List Multiple(Int32? numberOfObjects = null) /// or TimeSpan. /// /// A random scalar value. - public static Object ScalarValue() => + public static object ScalarValue() => faker.Random.Int(0, 14) switch { - 0 => fixture.Create(), - 1 => fixture.Create(), - 2 => fixture.Create(), + 0 => fixture.Create(), + 1 => fixture.Create(), + 2 => fixture.Create(), 3 => fixture.Create(), 4 => fixture.Create(), - 5 => fixture.Create(), - 6 => fixture.Create(), + 5 => fixture.Create(), + 6 => fixture.Create(), 7 => fixture.Create(), - 8 => fixture.Create(), - 9 => fixture.Create(), - 10 => fixture.Create(), - 11 => fixture.Create(), - 12 => fixture.Create(), + 8 => fixture.Create(), + 9 => fixture.Create(), + 10 => fixture.Create(), + 11 => fixture.Create(), + 12 => fixture.Create(), 13 => fixture.Create(), - _ => fixture.Create() + _ => fixture.Create(), }; /// @@ -247,8 +248,7 @@ public static T Single() /// Generates a random number between 5 and 15. /// /// A random number between 5 and 15. - public static Int32 SmallNumber() => - faker.Random.Int(5, 15); + public static int SmallNumber() => faker.Random.Int(5, 15); /// /// Creates a copy of where all properties except the key and concurrency token @@ -286,8 +286,7 @@ public static T UpdateFor(T entity) /// A list with copies of where all properties except key and concurrency token /// properties have new values. /// - public static List UpdateFor(List entities) => - [.. entities.Select(UpdateFor)]; + public static List UpdateFor(List entities) => [.. entities.Select(UpdateFor)]; /// /// Copies the values of all key and concurrency token properties from @@ -300,10 +299,9 @@ private static void CopyKeysAndConcurrencyTokens(T sourceEntity, T targetEnti { var metadata = EntityHelper.GetEntityTypeMetadata(typeof(T)); - var propertiesToCopy = - metadata.KeyProperties - .Concat(metadata.ConcurrencyTokenProperties) - .Concat(metadata.RowVersionProperties); + var propertiesToCopy = metadata + .KeyProperties.Concat(metadata.ConcurrencyTokenProperties) + .Concat(metadata.RowVersionProperties); foreach (var property in propertiesToCopy) { @@ -311,17 +309,6 @@ private static void CopyKeysAndConcurrencyTokens(T sourceEntity, T targetEnti } } - /// - /// The characters used for Char generation. - /// We only use alphabetic characters for Char generation to avoid issues with databases that do not support - /// certain characters. - /// - private static readonly Char[] characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray(); - - private static readonly Faker faker; - private static readonly Fixture fixture; - private static Int64 entityId = 1; - /// /// An AutoFixture customization that excludes properties that are ignored in the entity model from being populated /// with test data. @@ -333,7 +320,7 @@ public void Customize(IFixture fixture) => private class OmitNotMappedPropertySpecimenBuilder : ISpecimenBuilder { - public Object Create(Object request, ISpecimenContext context) + public object Create(object request, ISpecimenContext context) { if (request is PropertyInfo propertyInfo) { @@ -343,9 +330,7 @@ public Object Create(Object request, ISpecimenContext context) entityTypeMetadata.AllPropertiesByPropertyName.TryGetValue( propertyInfo.Name, out var propertyMetadata - ) - && - propertyMetadata.IsIgnored + ) && propertyMetadata.IsIgnored ) { return new OmitSpecimen(); diff --git a/tests/DbConnectionPlus.UnitTests/TestData/Item.cs b/tests/DbConnectionPlus.UnitTests/TestData/Item.cs index 798dbc0..98eaa0a 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/Item.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/Item.cs @@ -1,3 +1,3 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; -public record Item(Int64 Id, String Name, TestEnum Enum); +public record Item(long Id, string Name, TestEnum Enum); diff --git a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs index 84f0562..482d343 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithConstructor.cs @@ -1,19 +1,21 @@ // ReSharper disable ConvertToPrimaryConstructor +// An explicit public constructor is the whole point of this fixture: it is what the constructor-injection +// materializer binds to. A primary constructor would change what is under test. #pragma warning disable IDE0290 namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class ItemWithConstructor { - public ItemWithConstructor(Int16 a, Int32 b, Int64 c) + public ItemWithConstructor(short a, int b, long c) { this.A = a; this.B = b; this.C = c; } - public Int16 A { get; init; } - public Int32 B { get; init; } - public Int64 C { get; init; } + public short A { get; init; } + public int B { get; init; } + public long C { get; init; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithPrivateConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithPrivateConstructor.cs index e50c677..22e7ca7 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithPrivateConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithPrivateConstructor.cs @@ -2,14 +2,14 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public sealed class ItemWithPrivateConstructor { - private ItemWithPrivateConstructor(Int16 a, Int32 b, Int64 c) + private ItemWithPrivateConstructor(short a, int b, long c) { this.A = a; this.B = b; this.C = c; } - public Int16 A { get; init; } - public Int32 B { get; init; } - public Int64 C { get; init; } + public short A { get; init; } + public int B { get; init; } + public long C { get; init; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithPrivateParameterlessConstructor.cs b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithPrivateParameterlessConstructor.cs index 4affe93..d28ad5f 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/ItemWithPrivateParameterlessConstructor.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/ItemWithPrivateParameterlessConstructor.cs @@ -4,7 +4,5 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public sealed class ItemWithPrivateParameterlessConstructor { - private ItemWithPrivateParameterlessConstructor() - { - } + private ItemWithPrivateParameterlessConstructor() { } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs index 5712727..b0a00ed 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntity.cs @@ -2,13 +2,13 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record MappingTestEntity { - public Byte[]? ConcurrencyToken { get; set; } + public byte[]? ConcurrencyToken { get; set; } [Key] - public Int64 Key1 { get; set; } + public long Key1 { get; set; } [Key] - public Int64 Key2 { get; set; } + public long Key2 { get; set; } - public Int32 Value { get; set; } + public int Value { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs index 170e495..fddf169 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityAttributes.cs @@ -7,31 +7,31 @@ public record MappingTestEntityAttributes { [Column("Computed")] [DatabaseGenerated(DatabaseGeneratedOption.Computed)] - public Int32 Computed_ { get; set; } + public int Computed_ { get; set; } [Column("ConcurrencyToken")] [ConcurrencyCheck] - public Byte[]? ConcurrencyToken_ { get; set; } + public byte[]? ConcurrencyToken_ { get; set; } [Column("Identity")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Int32 Identity_ { get; set; } + public int Identity_ { get; set; } [Key] [Column("Key1")] - public Int64 Key1_ { get; set; } + public long Key1_ { get; set; } [Key] [Column("Key2")] - public Int64 Key2_ { get; set; } + public long Key2_ { get; set; } [NotMapped] - public String? NotMapped { get; set; } + public string? NotMapped { get; set; } [Column("RowVersion")] [Timestamp] - public Byte[]? RowVersion_ { get; set; } + public byte[]? RowVersion_ { get; set; } [Column("Value")] - public Int32 Value_ { get; set; } + public int Value_ { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs index 7e86898..d7ab793 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/MappingTestEntityFluentApi.cs @@ -4,61 +4,53 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public record MappingTestEntityFluentApi { - public Int32 Computed_ { get; set; } - public Byte[]? ConcurrencyToken_ { get; set; } - public Int32 Identity_ { get; set; } - public Int64 Key1_ { get; set; } - public Int64 Key2_ { get; set; } - public String? NotMapped { get; set; } - public Byte[]? RowVersion_ { get; set; } - public Int32 Value_ { get; set; } + public int Computed_ { get; set; } + public byte[]? ConcurrencyToken_ { get; set; } + public int Identity_ { get; set; } + public long Key1_ { get; set; } + public long Key2_ { get; set; } + public string? NotMapped { get; set; } + public byte[]? RowVersion_ { get; set; } + public int Value_ { get; set; } /// /// Configures the mapping for this entity using the Fluent API. /// public static void Configure() => DbConnectionExtensions.Configure(config => - { - config.Entity() - .ToTable("MappingTestEntity"); + { + config.Entity().ToTable("MappingTestEntity"); - config.Entity() - .Property(a => a.Computed_) - .HasColumnName("Computed") - .IsComputed(); + config + .Entity() + .Property(a => a.Computed_) + .HasColumnName("Computed") + .IsComputed(); - config.Entity() - .Property(a => a.ConcurrencyToken_) - .HasColumnName("ConcurrencyToken") - .IsConcurrencyToken(); + config + .Entity() + .Property(a => a.ConcurrencyToken_) + .HasColumnName("ConcurrencyToken") + .IsConcurrencyToken(); - config.Entity() - .Property(a => a.Identity_) - .HasColumnName("Identity") - .IsIdentity(); + config + .Entity() + .Property(a => a.Identity_) + .HasColumnName("Identity") + .IsIdentity(); - config.Entity() - .Property(a => a.Key1_) - .HasColumnName("Key1") - .IsKey(); + config.Entity().Property(a => a.Key1_).HasColumnName("Key1").IsKey(); - config.Entity() - .Property(a => a.Key2_) - .HasColumnName("Key2") - .IsKey(); + config.Entity().Property(a => a.Key2_).HasColumnName("Key2").IsKey(); - config.Entity() - .Property(a => a.Value_) - .HasColumnName("Value"); + config.Entity().Property(a => a.Value_).HasColumnName("Value"); - config.Entity() - .Property(a => a.NotMapped) - .IsIgnored(); + config.Entity().Property(a => a.NotMapped).IsIgnored(); - config.Entity() - .Property(a => a.RowVersion_) - .HasColumnName("RowVersion") - .IsRowVersion(); - } - ); + config + .Entity() + .Property(a => a.RowVersion_) + .HasColumnName("RowVersion") + .IsRowVersion(); + }); } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/NotAValueTuple.cs b/tests/DbConnectionPlus.UnitTests/TestData/NotAValueTuple.cs index a223a4e..8cfa2a5 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/NotAValueTuple.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/NotAValueTuple.cs @@ -6,18 +6,14 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public struct NotAValueTuple : IStructuralEquatable, IStructuralComparable, IComparable { /// - public Int32 CompareTo(Object? other, IComparer comparer) => - throw new NotImplementedException(); + public int CompareTo(object? obj) => throw new NotImplementedException(); /// - public Int32 CompareTo(Object? obj) => - throw new NotImplementedException(); + public int CompareTo(object? other, IComparer comparer) => throw new NotImplementedException(); /// - public Boolean Equals(Object? other, IEqualityComparer comparer) => - throw new NotImplementedException(); + public bool Equals(object? other, IEqualityComparer comparer) => throw new NotImplementedException(); /// - public Int32 GetHashCode(IEqualityComparer comparer) => - throw new NotImplementedException(); + public int GetHashCode(IEqualityComparer comparer) => throw new NotImplementedException(); } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItem.cs b/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItem.cs index 511e5db..d4f35be 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItem.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItem.cs @@ -2,19 +2,19 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class TemporaryTableTestItem { - public Boolean Boolean { get; set; } - public Byte[] Bytes { get; set; } = null!; - public Char Char { get; set; } + public bool Boolean { get; set; } + public byte[] Bytes { get; set; } = null!; + public char Char { get; set; } public DateOnly DateOnly { get; set; } public DateTime DateTime { get; set; } - public Decimal Decimal { get; set; } - public Double Double { get; set; } + public decimal Decimal { get; set; } + public double Double { get; set; } public Guid Guid { get; set; } - public Int16 Int16 { get; set; } - public Int32 Int32 { get; set; } - public Int64 Int64 { get; set; } - public Single Single { get; set; } - public String String { get; set; } = null!; + public short Int16 { get; set; } + public int Int32 { get; set; } + public long Int64 { get; set; } + public float Single { get; set; } + public string String { get; set; } = null!; public TimeOnly TimeOnly { get; set; } public TimeSpan TimeSpan { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItemWithNullableProperties.cs b/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItemWithNullableProperties.cs index 4d495e5..c77225c 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItemWithNullableProperties.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/TemporaryTableTestItemWithNullableProperties.cs @@ -2,19 +2,19 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.TestData; public class TemporaryTableTestItemWithNullableProperties { - public Boolean? Boolean { get; set; } - public Byte[]? Bytes { get; set; } - public Char? Char { get; set; } + public bool? Boolean { get; set; } + public byte[]? Bytes { get; set; } + public char? Char { get; set; } public DateOnly? DateOnly { get; set; } public DateTime? DateTime { get; set; } - public Decimal? Decimal { get; set; } - public Double? Double { get; set; } + public decimal? Decimal { get; set; } + public double? Double { get; set; } public Guid? Guid { get; set; } - public Int16? Int16 { get; set; } - public Int32? Int32 { get; set; } - public Int64? Int64 { get; set; } - public Single? Single { get; set; } - public String? String { get; set; } + public short? Int16 { get; set; } + public int? Int32 { get; set; } + public long? Int64 { get; set; } + public float? Single { get; set; } + public string? String { get; set; } public TimeOnly? TimeOnly { get; set; } public TimeSpan? TimeSpan { get; set; } } diff --git a/tests/DbConnectionPlus.UnitTests/TestData/TestEnum.cs b/tests/DbConnectionPlus.UnitTests/TestData/TestEnum.cs index 988010a..ef83dce 100644 --- a/tests/DbConnectionPlus.UnitTests/TestData/TestEnum.cs +++ b/tests/DbConnectionPlus.UnitTests/TestData/TestEnum.cs @@ -8,5 +8,5 @@ public enum TestEnum Value2 = 2, Value3 = 3, Value4 = 4, - Value5 = 5 + Value5 = 5, } diff --git a/tests/DbConnectionPlus.UnitTests/Trimming/ILLinkDescriptorsTests.cs b/tests/DbConnectionPlus.UnitTests/Trimming/ILLinkDescriptorsTests.cs index 706ca0e..95e126e 100644 --- a/tests/DbConnectionPlus.UnitTests/Trimming/ILLinkDescriptorsTests.cs +++ b/tests/DbConnectionPlus.UnitTests/Trimming/ILLinkDescriptorsTests.cs @@ -22,10 +22,14 @@ namespace RentADeveloper.DbConnectionPlus.UnitTests.Trimming; /// public class ILLinkDescriptorsTests : UnitTestsBase { + private const string ILLinkDescriptorsResourceName = "ILLink.Descriptors.xml"; + [Fact] public void CoreAssembly_ShouldEmbedTheILLinkDescriptor() => - typeof(DbConnectionExtensions).Assembly.GetManifestResourceNames() - .Should().Contain(ILLinkDescriptorsResourceName); + typeof(DbConnectionExtensions) + .Assembly.GetManifestResourceNames() + .Should() + .Contain(ILLinkDescriptorsResourceName); [Theory] [InlineData(1)] @@ -36,25 +40,23 @@ public void CoreAssembly_ShouldEmbedTheILLinkDescriptor() => [InlineData(6)] [InlineData(7)] [InlineData(8)] - public void ILLinkDescriptor_ShouldPreserveAllMembersOfEveryValueTupleArity(Int32 arity) + public void ILLinkDescriptor_ShouldPreserveAllMembersOfEveryValueTupleArity(int arity) { var preservedTypes = ReadDescriptor() .Descendants("type") - .Where(a => (String?)a.Attribute("preserve") == "all") - .Select(a => (String?)a.Attribute("fullname")) + .Where(a => (string?)a.Attribute("preserve") == "all") + .Select(a => (string?)a.Attribute("fullname")) .ToList(); - preservedTypes - .Should().Contain($"System.ValueTuple`{arity}"); + preservedTypes.Should().Contain($"System.ValueTuple`{arity}"); } private static XDocument ReadDescriptor() { - using var stream = typeof(DbConnectionExtensions).Assembly - .GetManifestResourceStream(ILLinkDescriptorsResourceName)!; + using var stream = typeof(DbConnectionExtensions).Assembly.GetManifestResourceStream( + ILLinkDescriptorsResourceName + )!; return XDocument.Load(stream); } - - private const String ILLinkDescriptorsResourceName = "ILLink.Descriptors.xml"; } diff --git a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs index 5b962ae..4d3e346 100644 --- a/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs +++ b/tests/DbConnectionPlus.UnitTests/UnitTestsBase.cs @@ -22,9 +22,9 @@ public UnitTestsBase() // Ensure consistent culture for tests. CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = - Thread.CurrentThread.CurrentCulture = - Thread.CurrentThread.CurrentUICulture = - new("en-US"); + Thread.CurrentThread.CurrentCulture = + Thread.CurrentThread.CurrentUICulture = + new("en-US"); this.MockDatabaseAdapter = Substitute.For(); this.MockEntityManipulator = Substitute.For(); @@ -50,73 +50,67 @@ public UnitTestsBase() this.MockTemporaryTableBuilder = Substitute.For(); this.MockTemporaryTableBuilder.BuildTemporaryTable( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any() - ).Returns(new TemporaryTableDisposer(Substitute.For(), Substitute.For>())); + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) + .Returns(new TemporaryTableDisposer(Substitute.For(), Substitute.For>())); this.MockTemporaryTableBuilder.BuildTemporaryTableAsync( - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any(), - Arg.Any() - ).Returns(new TemporaryTableDisposer(Substitute.For(), Substitute.For>())); + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) + .Returns(new TemporaryTableDisposer(Substitute.For(), Substitute.For>())); this.MockDatabaseAdapter.SupportsTemporaryTables(Arg.Any()).Returns(true); this.MockDatabaseAdapter.TemporaryTableBuilder.Returns(this.MockTemporaryTableBuilder); - this.MockDatabaseAdapter.QuoteIdentifier(Arg.Any()) - .Returns(info => $"[{info.ArgAt(0)}]"); + this.MockDatabaseAdapter.QuoteIdentifier(Arg.Any()).Returns(info => $"[{info.ArgAt(0)}]"); - this.MockDatabaseAdapter.QuoteTemporaryTableName(Arg.Any(), this.MockDbConnection) - .Returns(info => $"[#{info.ArgAt(0)}]"); + this.MockDatabaseAdapter.QuoteTemporaryTableName(Arg.Any(), this.MockDbConnection) + .Returns(info => $"[#{info.ArgAt(0)}]"); - this.MockDatabaseAdapter.FormatParameterName(Arg.Any()) - .Returns(info => $"@{info.ArgAt(0)}"); + this.MockDatabaseAdapter.FormatParameterName(Arg.Any()).Returns(info => $"@{info.ArgAt(0)}"); - this.MockDatabaseAdapter - .When(a => a.BindParameterValue(Arg.Any(), Arg.Any())) + this.MockDatabaseAdapter.When(a => a.BindParameterValue(Arg.Any(), Arg.Any())) .Do(info => - { - var parameter = info.ArgAt(0); - var value = info.ArgAt(1); + { + var parameter = info.ArgAt(0); + var value = info.ArgAt(1); - if (value is Enum enumValue) - { - parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch - { - EnumSerializationMode.Integers => - DbType.Int32, - - EnumSerializationMode.Strings => - DbType.String, - - _ => - throw new NotSupportedException( - $"The {nameof(EnumSerializationMode)} " + - $"{DbConnectionPlusConfiguration.Instance.EnumSerializationMode.ToDebugString()} " + - "is not supported." - ) - }; - - parameter.Value = - EnumSerializer.SerializeEnum( - enumValue, - DbConnectionPlusConfiguration.Instance.EnumSerializationMode - ); - } - else + if (value is Enum enumValue) + { + parameter.DbType = DbConnectionPlusConfiguration.Instance.EnumSerializationMode switch { - parameter.Value = value ?? DBNull.Value; - } + EnumSerializationMode.Integers => DbType.Int32, + + EnumSerializationMode.Strings => DbType.String, + + _ => throw new NotSupportedException( + $"The {nameof(EnumSerializationMode)} " + + $"{DbConnectionPlusConfiguration.Instance.EnumSerializationMode.ToDebugString()} " + + "is not supported." + ), + }; + + parameter.Value = EnumSerializer.SerializeEnum( + enumValue, + DbConnectionPlusConfiguration.Instance.EnumSerializationMode + ); + } + else + { + parameter.Value = value ?? DBNull.Value; } - ); + }); this.MockDatabaseAdapter.EntityManipulator.Returns(this.MockEntityManipulator); @@ -126,12 +120,13 @@ public UnitTestsBase() DbConnectionPlusConfiguration.Instance = new() { EnumSerializationMode = EnumSerializationMode.Strings, - InterceptDbCommand = this.MockInterceptDbCommand + InterceptDbCommand = this.MockInterceptDbCommand, }; EntityHelper.ResetEntityTypeMetadataCache(); OracleDatabaseAdapter.AllowTemporaryTables = false; - typeof(DbConnectionPlusConfiguration).GetMethod(nameof(DbConnectionPlusConfiguration.RegisterDatabaseAdapter))! + typeof(DbConnectionPlusConfiguration) + .GetMethod(nameof(DbConnectionPlusConfiguration.RegisterDatabaseAdapter))! .MakeGenericMethod(this.MockDbConnection.GetType()) .Invoke(DbConnectionPlusConfiguration.Instance, [this.MockDatabaseAdapter]); } diff --git a/tests/package-consumption/AllAdaptersConsumer/GlobalUsings.cs b/tests/package-consumption/AllAdaptersConsumer/GlobalUsings.cs index 1e6981f..76b40b1 100644 --- a/tests/package-consumption/AllAdaptersConsumer/GlobalUsings.cs +++ b/tests/package-consumption/AllAdaptersConsumer/GlobalUsings.cs @@ -2,5 +2,5 @@ global using System.Data.Common; global using Microsoft.Data.Sqlite; global using RentADeveloper.DbConnectionPlus.Configuration; -global using RentADeveloper.DbConnectionPlus.PackageConsumption; global using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; +global using RentADeveloper.DbConnectionPlus.PackageConsumption; diff --git a/tests/package-consumption/AllAdaptersConsumer/Program.cs b/tests/package-consumption/AllAdaptersConsumer/Program.cs index 23bbf8e..1ad668b 100644 --- a/tests/package-consumption/AllAdaptersConsumer/Program.cs +++ b/tests/package-consumption/AllAdaptersConsumer/Program.cs @@ -40,9 +40,9 @@ public static class Program { /// The entry point. /// Zero if every assertion passed, otherwise one. - public static Int32 Main() + public static int Main() { - Console.WriteLine(new String('=', 100)); + Console.WriteLine(new string('=', 100)); Console.WriteLine("DbConnectionPlus - all-adapters package consumer"); Console.WriteLine(); Console.WriteLine($" runtime {Environment.Version}"); @@ -67,7 +67,7 @@ public static Int32 Main() } Console.WriteLine(); - Console.WriteLine(new String('=', 100)); + Console.WriteLine(new string('=', 100)); if (Check.FailureCount == 0) { @@ -99,12 +99,7 @@ private static void RegisterEveryAdapter() DbConnectionPlusConfiguration? configured = null; Configure(configuration => - configured = configuration - .UseMySql() - .UseOracle() - .UsePostgreSql() - .UseSqlite() - .UseSqlServer() + configured = configuration.UseMySql().UseOracle().UsePostgreSql().UseSqlite().UseSqlServer() ); Check.True("the five UseXxx calls chain and return the configuration", configured is not null); @@ -143,7 +138,7 @@ private static void AssertDriverPackagesFlowedTransitively() /// The name of the adapter the driver belongs to. /// The freshly constructed, unopened connection. /// The simple name of the assembly the type must come from. - private static void AssertConnectionType(String adapter, DbConnection connection, String expectedAssemblyName) + private static void AssertConnectionType(string adapter, DbConnection connection, string expectedAssemblyName) { using (connection) { @@ -170,7 +165,8 @@ private static void AssertOneSharedLibraryAssembly() { Check.Section("3. One shared DbConnectionPlus assembly, not one copy per adapter"); - var libraryAssemblies = AppDomain.CurrentDomain.GetAssemblies() + var libraryAssemblies = AppDomain + .CurrentDomain.GetAssemblies() .Select(assembly => assembly.GetName()) .Where(name => name.Name == "RentADeveloper.DbConnectionPlus") .ToList(); @@ -236,8 +232,8 @@ public sealed class Widget { /// The primary key. Not database-generated, so it takes part in the INSERT. [Key] - public Int64 Id { get; set; } + public long Id { get; set; } /// A plain string column. - public String Name { get; set; } = String.Empty; + public string Name { get; set; } = string.Empty; } diff --git a/tests/package-consumption/AotConsumer/GlobalUsings.cs b/tests/package-consumption/AotConsumer/GlobalUsings.cs index 5ce6699..e0af278 100644 --- a/tests/package-consumption/AotConsumer/GlobalUsings.cs +++ b/tests/package-consumption/AotConsumer/GlobalUsings.cs @@ -2,6 +2,6 @@ global using System.Data.Common; global using Microsoft.Data.Sqlite; global using RentADeveloper.DbConnectionPlus.Configuration; +global using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; global using RentADeveloper.DbConnectionPlus.PackageConsumption; global using RentADeveloper.DbConnectionPlus.SqlStatements; -global using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions; diff --git a/tests/package-consumption/AotConsumer/Model.cs b/tests/package-consumption/AotConsumer/Model.cs index 8c62db5..48f7e7a 100644 --- a/tests/package-consumption/AotConsumer/Model.cs +++ b/tests/package-consumption/AotConsumer/Model.cs @@ -38,7 +38,7 @@ public enum SmokeStatus Active = 1, /// The entity is retained for reference only. - Archived = 2 + Archived = 2, } /// @@ -49,16 +49,16 @@ public sealed class SmokeEntity { /// The primary key. Not database-generated, so it takes part in the INSERT. [Key] - public Int64 Id { get; set; } + public long Id { get; set; } /// A plain string column. - public String Name { get; set; } = String.Empty; + public string Name { get; set; } = string.Empty; /// Stored as TEXT by SQLite, so materializing it exercises a String to Decimal conversion. - public Decimal Balance { get; set; } + public decimal Balance { get; set; } /// Stored as INTEGER by SQLite, so materializing it exercises an Int64 to Boolean conversion. - public Boolean IsActive { get; set; } + public bool IsActive { get; set; } /// Stored as TEXT by SQLite, so materializing it exercises a String to DateTime conversion. public DateTime CreatedAt { get; set; } @@ -70,7 +70,7 @@ public sealed class SmokeEntity public SmokeStatus Status { get; set; } /// SQLite reports INTEGER columns as , so this narrows on materialization. - public Int32 Quantity { get; set; } + public int Quantity { get; set; } } /// @@ -83,7 +83,7 @@ public sealed class ImmutableSmokeEntity /// The primary key. /// The name. /// The balance. - public ImmutableSmokeEntity(Int64 id, String name, Decimal balance) + public ImmutableSmokeEntity(long id, string name, decimal balance) { this.Id = id; this.Name = name; @@ -91,13 +91,13 @@ public ImmutableSmokeEntity(Int64 id, String name, Decimal balance) } /// The primary key. - public Int64 Id { get; } + public long Id { get; } /// The name. - public String Name { get; } + public string Name { get; } /// The balance. - public Decimal Balance { get; } + public decimal Balance { get; } } // ===================================================================================================== @@ -134,7 +134,7 @@ public enum FlatTupleNumericEnum FlatNumericChosen = 71, /// Never stored. Present so that an off-by-one bind is visible. - FlatNumericOther = 72 + FlatNumericOther = 72, } /// @@ -150,7 +150,7 @@ public enum NestedTupleNumericEnum NestedNumericChosen = 81, /// Never stored. Present so that an off-by-one bind is visible. - NestedNumericOther = 82 + NestedNumericOther = 82, } /// @@ -166,7 +166,7 @@ public enum FlatTupleNamedEnum FlatNamedChosen = 91, /// Never stored. Present so that a parse landing on the wrong member is visible. - FlatNamedOther = 92 + FlatNamedOther = 92, } /// @@ -184,7 +184,7 @@ public enum NestedTupleNamedEnum NestedNamedChosen = 101, /// Never stored. Present so that a parse landing on the wrong member is visible. - NestedNamedOther = 102 + NestedNamedOther = 102, } /// @@ -194,8 +194,8 @@ public sealed class SmokeItem { /// The primary key. [Key] - public Int64 Id { get; set; } + public long Id { get; set; } /// The label of the item. - public String Label { get; set; } = String.Empty; + public string Label { get; set; } = string.Empty; } diff --git a/tests/package-consumption/AotConsumer/Program.cs b/tests/package-consumption/AotConsumer/Program.cs index 7f15268..ef18888 100644 --- a/tests/package-consumption/AotConsumer/Program.cs +++ b/tests/package-consumption/AotConsumer/Program.cs @@ -30,14 +30,11 @@ public static class Program { /// The entry point. /// Zero if every assertion passed, otherwise one. - public static Int32 Main() + public static int Main() { - var databasePath = Path.Combine( - Path.GetTempPath(), - $"dbconnectionplus-aot-consumer-{Guid.NewGuid():N}.db" - ); + var databasePath = Path.Combine(Path.GetTempPath(), $"dbconnectionplus-aot-consumer-{Guid.NewGuid():N}.db"); - Console.WriteLine(new String('=', 100)); + Console.WriteLine(new string('=', 100)); Console.WriteLine("DbConnectionPlus - Native AOT package consumer"); Console.WriteLine(); var packageAssembly = typeof(DbConnectionPlusConfiguration).Assembly.GetName(); @@ -93,7 +90,7 @@ public static Int32 Main() } Console.WriteLine(); - Console.WriteLine(new String('=', 100)); + Console.WriteLine(new string('=', 100)); if (Check.FailureCount == 0) { diff --git a/tests/package-consumption/AotConsumer/SmokeCases.cs b/tests/package-consumption/AotConsumer/SmokeCases.cs index 7f08df1..42b6421 100644 --- a/tests/package-consumption/AotConsumer/SmokeCases.cs +++ b/tests/package-consumption/AotConsumer/SmokeCases.cs @@ -25,11 +25,11 @@ public static class SmokeCases CreatedAt = new DateTime(2026, 8, 11, 12, 34, 56, DateTimeKind.Unspecified), ExternalId = new Guid("6f9619ff-8b86-d011-b42d-00cf4fc964ff"), Status = SmokeStatus.Active, - Quantity = 42 + Quantity = 42, }; /// The primary key of the single row the enum cases read. - private const Int64 EnumRowId = 1; + private const long EnumRowId = 1; /// /// Creates the tables the remaining cases read from, and writes the row the enum cases read. @@ -95,7 +95,7 @@ public static void InsertEntity(DbConnection connection) Check.Equal( "the row is readable again", 1L, - connection.ExecuteScalar("SELECT COUNT(*) FROM SmokeEntity") + connection.ExecuteScalar("SELECT COUNT(*) FROM SmokeEntity") ); } @@ -118,13 +118,15 @@ public static void QueryEntity(DbConnection connection) { Check.Section("2. Query - property-setter strategy, every property asserted"); - var entity = connection.Query( - $""" - SELECT Id, Name, Balance, IsActive, CreatedAt, ExternalId, Status, Quantity - FROM SmokeEntity - WHERE Id = {ExpectedEntity.Id} - """ - ).Single(); + var entity = connection + .Query( + $""" + SELECT Id, Name, Balance, IsActive, CreatedAt, ExternalId, Status, Quantity + FROM SmokeEntity + WHERE Id = {ExpectedEntity.Id} + """ + ) + .Single(); Check.Equal("Id", ExpectedEntity.Id, entity.Id); Check.Equal("Name", ExpectedEntity.Name, entity.Name); @@ -152,9 +154,9 @@ public static void QueryEntityWithADifferentSelectList(DbConnection connection) { Check.Section("3. Query - different result-set shape (reordered, partial SELECT list)"); - var entity = connection.Query( - $"SELECT Quantity, Name, Id FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}" - ).Single(); + var entity = connection + .Query($"SELECT Quantity, Name, Id FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}") + .Single(); Check.Equal("Quantity", ExpectedEntity.Quantity, entity.Quantity); Check.Equal("Name", ExpectedEntity.Name, entity.Name); @@ -169,9 +171,9 @@ public static void QueryImmutableEntity(DbConnection connection) { Check.Section("4. Query - constructor injection"); - var entity = connection.Query( - $"SELECT Id, Name, Balance FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}" - ).Single(); + var entity = connection + .Query($"SELECT Id, Name, Balance FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}") + .Single(); Check.Equal("Id", ExpectedEntity.Id, entity.Id); Check.Equal("Name", ExpectedEntity.Name, entity.Name); @@ -182,11 +184,13 @@ public static void QueryImmutableEntity(DbConnection connection) /// The open connection to the temporary SQLite database. public static void QueryValueTuple(DbConnection connection) { - Check.Section("5. Query<(Int64, String, Decimal)> - value tuple"); + Check.Section("5. Query<(long, string, decimal)> - value tuple"); - var (id, name, balance) = connection.Query<(Int64 Id, String Name, Decimal Balance)>( - $"SELECT Id, Name, Balance FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}" - ).Single(); + var (id, name, balance) = connection + .Query<(long Id, string Name, decimal Balance)>( + $"SELECT Id, Name, Balance FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}" + ) + .Single(); Check.Equal("Id", ExpectedEntity.Id, id); Check.Equal("Name", ExpectedEntity.Name, name); @@ -208,16 +212,18 @@ public static void QueryNestedValueTuple(DbConnection connection) { Check.Section("6. Query<(...8 fields)> - nested value tuple (TRest)"); - var tuple = connection.Query<(Int64 A, Int64 B, Int64 C, Int64 D, Int64 E, Int64 F, Int64 G, String H)>( - $""" - SELECT Id AS A, Quantity AS B, Id AS C, Quantity AS D, Id AS E, Quantity AS F, Id AS G, Name AS H - FROM SmokeEntity - WHERE Id = {ExpectedEntity.Id} - """ - ).Single(); + var tuple = connection + .Query<(long A, long B, long C, long D, long E, long F, long G, string H)>( + $""" + SELECT Id AS A, Quantity AS B, Id AS C, Quantity AS D, Id AS E, Quantity AS F, Id AS G, Name AS H + FROM SmokeEntity + WHERE Id = {ExpectedEntity.Id} + """ + ) + .Single(); Check.Equal("field 1", ExpectedEntity.Id, tuple.A); - Check.Equal("field 2", (Int64)ExpectedEntity.Quantity, tuple.B); + Check.Equal("field 2", (long)ExpectedEntity.Quantity, tuple.B); Check.Equal("field 7", ExpectedEntity.Id, tuple.G); Check.Equal("field 8 (nested in TRest)", ExpectedEntity.Name, tuple.H); } @@ -238,7 +244,7 @@ public static void QueryDataRow(DbConnection connection) var row = connection.Query($"SELECT Id, Name FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}").Single(); Check.Equal("row[\"Id\"]", ExpectedEntity.Id, Convert.ToInt64(row["Id"], null)); - Check.Equal("row[\"Name\"]", ExpectedEntity.Name, row["Name"] as String); + Check.Equal("row[\"Name\"]", ExpectedEntity.Name, row["Name"] as string); } /// Streams a single-column temporary table into the database and reads it back. @@ -247,11 +253,9 @@ public static void SingleColumnTemporaryTable(DbConnection connection) { Check.Section("8. TemporaryTable - single column"); - var values = new List { 10, 20, 30 }; + var values = new List { 10, 20, 30 }; - var read = connection.Query( - $"SELECT Value FROM {TemporaryTable(values)} ORDER BY Value" - ).ToList(); + var read = connection.Query($"SELECT Value FROM {TemporaryTable(values)} ORDER BY Value").ToList(); Check.Equal("three values round-trip", 3, read.Count); Check.True("the values are unchanged", read.SequenceEqual(values)); @@ -272,12 +276,10 @@ public static void MultiColumnTemporaryTable(DbConnection connection) var items = new List { new() { Id = 1, Label = "one" }, - new() { Id = 2, Label = "two" } + new() { Id = 2, Label = "two" }, }; - var read = connection.Query( - $"SELECT Id, Label FROM {TemporaryTable(items)} ORDER BY Id" - ).ToList(); + var read = connection.Query($"SELECT Id, Label FROM {TemporaryTable(items)} ORDER BY Id").ToList(); Check.Equal("two items round-trip", 2, read.Count); Check.Equal("item 1 Id", 1L, read[0].Id); @@ -308,9 +310,10 @@ public static void ZeroBindingGuard(DbConnection connection) Check.Throws( "a result set matching no property throws instead of returning default-valued entities", "could be mapped to a writable property of the entity type", - () => connection.Query( - $"SELECT 1 AS Alpha, 2 AS Beta FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}" - ).ToList() + () => + connection + .Query($"SELECT 1 AS Alpha, 2 AS Beta FROM SmokeEntity WHERE Id = {ExpectedEntity.Id}") + .ToList() ); } @@ -329,14 +332,16 @@ public static void ZeroBindingGuard(DbConnection connection) /// public static void QueryValueTupleWithANumericEnum(DbConnection connection) { - Check.Section("11. Query<(Int64, enum)> - enum field of a flat value tuple, from an INTEGER column"); + Check.Section("11. Query<(long, enum)> - enum field of a flat value tuple, from an INTEGER column"); - var (id, status) = connection.Query<(Int64 Id, FlatTupleNumericEnum Status)>( - $"SELECT Id, FlatNumeric AS Status FROM SmokeEnum WHERE Id = {EnumRowId}" - ).Single(); + var (id, status) = connection + .Query<(long Id, FlatTupleNumericEnum Status)>( + $"SELECT Id, FlatNumeric AS Status FROM SmokeEnum WHERE Id = {EnumRowId}" + ) + .Single(); Check.Equal("Id", EnumRowId, id); - Check.Equal("the enum field binds the stored value", 71, (Int32)status); + Check.Equal("the enum field binds the stored value", 71, (int)status); } /// @@ -355,18 +360,18 @@ public static void QueryNestedValueTupleWithANumericEnum(DbConnection connection { Check.Section("12. Query<(...8 fields)> - enum field nested in TRest, from an INTEGER column"); - var tuple = connection.Query<( - Int64 A, Int64 B, Int64 C, Int64 D, Int64 E, Int64 F, Int64 G, NestedTupleNumericEnum H - )>( - $""" - SELECT Id AS A, Id AS B, Id AS C, Id AS D, Id AS E, Id AS F, Id AS G, NestedNumeric AS H - FROM SmokeEnum - WHERE Id = {EnumRowId} - """ - ).Single(); + var tuple = connection + .Query<(long A, long B, long C, long D, long E, long F, long G, NestedTupleNumericEnum H)>( + $""" + SELECT Id AS A, Id AS B, Id AS C, Id AS D, Id AS E, Id AS F, Id AS G, NestedNumeric AS H + FROM SmokeEnum + WHERE Id = {EnumRowId} + """ + ) + .Single(); Check.Equal("field 1", EnumRowId, tuple.A); - Check.Equal("field 8 (enum nested in TRest) binds the stored value", 81, (Int32)tuple.H); + Check.Equal("field 8 (enum nested in TRest) binds the stored value", 81, (int)tuple.H); } /// @@ -383,14 +388,16 @@ FROM SmokeEnum /// public static void QueryValueTupleWithANamedEnum(DbConnection connection) { - Check.Section("13. Query<(Int64, enum)> - enum field of a flat value tuple, parsed from a TEXT column"); + Check.Section("13. Query<(long, enum)> - enum field of a flat value tuple, parsed from a TEXT column"); - var (id, status) = connection.Query<(Int64 Id, FlatTupleNamedEnum Status)>( - $"SELECT Id, FlatNamed AS Status FROM SmokeEnum WHERE Id = {EnumRowId}" - ).Single(); + var (id, status) = connection + .Query<(long Id, FlatTupleNamedEnum Status)>( + $"SELECT Id, FlatNamed AS Status FROM SmokeEnum WHERE Id = {EnumRowId}" + ) + .Single(); Check.Equal("Id", EnumRowId, id); - Check.Equal("the name in the column parsed to the right member", 91, (Int32)status); + Check.Equal("the name in the column parsed to the right member", 91, (int)status); Check.Equal("the member name survived trimming", "FlatNamedChosen", status.ToString()); } @@ -411,18 +418,18 @@ public static void QueryNestedValueTupleWithANamedEnum(DbConnection connection) { Check.Section("14. Query<(...8 fields)> - enum field nested in TRest, parsed from a TEXT column"); - var tuple = connection.Query<( - Int64 A, Int64 B, Int64 C, Int64 D, Int64 E, Int64 F, Int64 G, NestedTupleNamedEnum H - )>( - $""" - SELECT Id AS A, Id AS B, Id AS C, Id AS D, Id AS E, Id AS F, Id AS G, NestedNamed AS H - FROM SmokeEnum - WHERE Id = {EnumRowId} - """ - ).Single(); + var tuple = connection + .Query<(long A, long B, long C, long D, long E, long F, long G, NestedTupleNamedEnum H)>( + $""" + SELECT Id AS A, Id AS B, Id AS C, Id AS D, Id AS E, Id AS F, Id AS G, NestedNamed AS H + FROM SmokeEnum + WHERE Id = {EnumRowId} + """ + ) + .Single(); Check.Equal("field 1", EnumRowId, tuple.A); - Check.Equal("field 8 (enum nested in TRest) parsed to the right member", 101, (Int32)tuple.H); + Check.Equal("field 8 (enum nested in TRest) parsed to the right member", 101, (int)tuple.H); Check.Equal("the member name survived trimming", "NestedNamedChosen", tuple.H.ToString()); } } diff --git a/tests/package-consumption/Check.cs b/tests/package-consumption/Check.cs index 1673a5b..11ad626 100644 --- a/tests/package-consumption/Check.cs +++ b/tests/package-consumption/Check.cs @@ -24,14 +24,14 @@ namespace RentADeveloper.DbConnectionPlus.PackageConsumption; public static class Check { /// Gets the number of assertions that failed so far. - public static Int32 FailureCount { get; private set; } + public static int FailureCount { get; private set; } /// Writes a section header, so the console output stays readable in a CI log. /// The title of the section. - public static void Section(String title) + public static void Section(string title) { Console.WriteLine(); - Console.WriteLine(new String('=', 100)); + Console.WriteLine(new string('=', 100)); Console.WriteLine(title); Console.WriteLine(); } @@ -41,7 +41,7 @@ public static void Section(String title) /// A description of what is being asserted. /// The expected value. /// The actual value. - public static void Equal(String label, T expected, T actual) + public static void Equal(string label, T expected, T actual) { if (EqualityComparer.Default.Equals(expected, actual)) { @@ -56,7 +56,7 @@ public static void Equal(String label, T expected, T actual) /// Asserts that is . /// A description of what is being asserted. /// The condition that must hold. - public static void True(String label, Boolean condition) + public static void True(string label, bool condition) { if (condition) { @@ -76,7 +76,7 @@ public static void True(String label, Boolean condition) /// A description of what is being asserted. /// A fragment the exception message must contain. /// The action that must throw. - public static void Throws(String label, String expectedMessageFragment, Action action) + public static void Throws(string label, string expectedMessageFragment, Action action) where TException : Exception { try @@ -108,10 +108,9 @@ public static void Throws(String label, String expectedMessageFragme Fail(label, $"expected {typeof(TException).Name}, but nothing was thrown"); } - private static void Pass(String label) => - Console.WriteLine($" PASS {label}"); + private static void Pass(string label) => Console.WriteLine($" PASS {label}"); - private static void Fail(String label, String detail) + private static void Fail(string label, string detail) { FailureCount++; @@ -119,12 +118,12 @@ private static void Fail(String label, String detail) Console.WriteLine($" {detail}"); } - private static String Render(Object? value) => + private static string Render(object? value) => value switch { null => "null", - Byte[] bytes => Convert.ToHexString(bytes), + byte[] bytes => Convert.ToHexString(bytes), IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), - _ => value.ToString() ?? String.Empty + _ => value.ToString() ?? string.Empty, }; }