You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 44059a5
Browse filesBrowse the repository at this point in the historyBrowse files
Docs: state the NULL policy and the async transaction token correctly; fix the struct constructor sentence (#137)
* Docs: NULL policy in one place, async transaction token, struct constructor, constructor rules, excluding a member
State the NULL policy once (mapping.md, "NULL in a non-nullable column") and point the other pages at it, including the single-value case (Map<int>().Single() over NULL is 0; Dapper's QuerySingle<int> throws). State that the async transaction helpers check the token after the action and send the commit without it. Correct the sentence that said a struct's own parameterless constructor is not called (it is), and the test that claimed the same. Add two lines to the constructor rule (a record with a second public constructor; an all-optional constructor is not parameterless) and one to the exclusion recipe (new(nameof(X.Member), null) needs no null!). Every new claim has a sample check.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Docs samples: add a missing space in the Dapper NULL check
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Docs samples: drop the struct constructor sample (the Core test covers it)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: Tests/Open.Database.Extensions.Core.Tests/UnmappableTypeTests.cs
+11-4Lines changed: 11 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -82,12 +82,13 @@ public class OneOfSeveralShape
82
82
publicstring?Built{get;}
83
83
}
84
84
85
-
// A struct with its own public parameterless constructor is built as ever, whatever else it declares: that constructor is not called, as it never was.
85
+
// A struct with its own public parameterless constructor is built with new(), whatever else it declares: that constructor is called, then the properties are set.
Assert.Equal((7,"parameterless constructor"),(mapped.Id,mapped.Source));// Source is set only by the constructor, so it ran, and the column was set after it.
Copy file name to clipboardExpand all lines: docsite/articles/coming-from-dapper.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -44,7 +44,7 @@ What is different:
44
44
-**The connection.** With Dapper you create it and dispose it, and Dapper opens it if it is closed and closes it when the call finishes. With a factory, the factory creates it for each execution, the command opens it only if it is closed and disposes it when the terminal returns, so it is open only for the duration of the call.
45
45
-**Rows.** Dapper's `QueryAsync<T>` buffers by default (`buffered = true`) and returns a task of `IEnumerable<T>`, turned into a list in the sample by Dapper's `AsList()`. Unlike `QueryAsync<T>`, `Map<T>()` runs nothing by itself: the query runs when a terminal such as `ToListAsync()` is called. `ToListAsync()` reads every row into a list during the call. `Read(handler)` offers a lazy sequence that is only valid inside the handler.
46
46
-**Mapping.** Both match columns to members by name, ignoring case. Dapper can also bind constructor parameters and fields and, if you turn on `MatchNamesWithUnderscores`, match `first_name` to `FirstName`. The extensions set public settable properties (`init` and `required` ones too) and bind the one public constructor of a record or class (see [Positional records](#positional-records)), but never set fields. They always ignore underscores when no exact or ignore-case match exists (`first_name` feeds `FirstName`; there is nothing to turn on). A column that differs by more than case and underscores needs a `RowMap<T>` (see [Renamed columns](#renamed-columns)).
47
-
-**NULL.** A `NULL`in a nullable property (`int?`, `string?`), or in a reference-type property in code with nullable annotations off, gives `null`. In a property the type declares non-nullable (`int`, `bool`, `DateTime`, an enum, or a `string` with `<Nullable>enable</Nullable>`) it leaves the initializer, else the default, and nothing throws: declare the property `int?` when you need to tell NULL from 0. See [Types and NULL](mapping.md#types-and-null).
47
+
-**NULL.** A `NULL`into a nullable member gives `null`, and into a non-nullable one its initializer, else its default; nothing throws (see [NULL in a non-nullable column](mapping.md#null-in-a-non-nullable-column)). Dapper's `QuerySingle<int>` over a `NULL` throws; `Map<int>().Single()` gives `0`. Declare `int?` when you need to tell NULL from 0.
48
48
-**Null parameters.** The extensions send a `null` value as a database NULL, and `AddParam(note)` also sends the argument's database type (see [Name a parameter from its argument](expressive-commands.md#name-a-parameter-from-its-argument)).
49
49
-**Mapping errors.** Dapper wraps them in a `DataException` ("Error parsing column ..."). The extensions throw a `ValueMappingException` (also a `DataException`) that names the property and the column.
The async forms (`ExecuteTransactionAsync`, `ExecuteTransactionConditionalAsync`) take the token too. The token is checked once more after your action returns: a token cancelled by then throws `OperationCanceledException` and nothing commits. The commit itself is sent without the token, so cancelling while it runs does not interrupt it (an interrupted commit would leave the outcome unknown).
The foundation also includes helpers you can use without the expressive syntax: `ForEach` and `ForEachAsync` over a reader, `Select` and `SelectAsync`, `AsEnumerable` and `AsAsyncEnumerable` (rows as `object[]`), `ToList`, `ToArray`, `ToDataTable` and friends on commands. Buffered reads copy the rows out of the reader first, so the connection can be released early: `Retrieve()` copies every row into a queue of `object[]` (raw rows with column names; `DequeueAsMappedDictionaries()` turns them into dictionaries). `DequeueAs<T>()`, which mapped that queue to `T`, is deprecated: use `Map<T>().ToList()`. `ResultsBuffered<T>()` (and `Results<T>()` on an `IDbCommand`, which buffers the rows and stays) buffers and maps in one call; `Results<T>()` on an `IExecuteReader` is deprecated, use `Map<T>().ToList()`. `FirstOrdinalResults<T>()` reads the first column of every row as `T`. On a command, `Map<int>()` (or `ToListAsync<int>()`) reads the first column as `int` with the mapper's conversions, and `FirstOrdinalResults<T>()` is the form for a raw command and a reader (see [One value per row](mapping.md#one-value-per-row)). For typed rows `Map<T>()` is the better route (see [Performance](performance.md#where-it-is-not-faster)).
Copy file name to clipboardExpand all lines: docsite/articles/mapping.md
+11-5Lines changed: 11 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -11,14 +11,14 @@
11
11
| Named differently | Declare a `RowMap<T>` once and pass it: `Map<T>(map)`|
12
12
| One property must not be read | A pair whose column is the C# `null` (not SQL NULL) in the `RowMap<T>`|
13
13
| A property must always be read | Declare it `required` ([`required` and `init` members](#required-and-init-members)) |
14
-
| A column can hold NULL | Declare the property nullable (`int?`, `string?`); a property the type declares non-nullable (`int`, or a `string` with `<Nullable>enable</Nullable>`) keeps its initializer, else its default|
14
+
| A column can hold NULL | Declare the property nullable (`int?`, `string?`): see [NULL in a non-nullable column](#null-in-a-non-nullable-column)|
15
15
| Your own conversion |`Map(selector)`|
16
16
17
17
## The rules
18
18
19
19
-**Construction.** A row is built through the type's constructor or from its default value, as the table in [Which types are built through a constructor](#which-types-are-built-through-a-constructor) says, and then its properties are set; any other type throws `InvalidOperationException` at the call, even for an empty result (see [Errors](#errors)).
20
20
-**What is set.** The parameters of the constructor, when the type is built through one, and the public instance properties that have a public setter (a record's `init` property included). A parameter and a property of the same name, ignoring case, are one member: it is fed once, through the constructor. Public fields and static properties are not mapped.
21
-
-**Matching.** A column is matched to the property or constructor parameter with the same name: exactly, else ignoring case, else ignoring case with the underscores removed from both (see [snake_case columns map to PascalCase properties](#snake_case-columns-map-to-pascalcase-properties)). A column with no matching member is ignored. A property with no matching column keeps the value its constructor gave it. A parameter with no matching column gets its declared default value, else its default (`0`, `null`); so does a parameter the type declares non-nullable (a value type, or a reference type with nullable annotations on) when the cell is `NULL`.
21
+
-**Matching.** A column is matched to the property or constructor parameter with the same name: exactly, else ignoring case, else ignoring case with the underscores removed from both (see [snake_case columns map to PascalCase properties](#snake_case-columns-map-to-pascalcase-properties)). A column with no matching member is ignored. A property with no matching column keeps the value its constructor gave it. A parameter with no matching column gets its declared default value, else its default (`0`, `null`). A `NULL` cell follows [the NULL rules](#null-in-a-non-nullable-column).
22
22
-**Columns named differently.** Declare a `RowMap<T>` once that names the column for a property, or ignores a property (see [Columns named differently](#columns-named-differently)).
23
23
-**No other conventions.** The library has no mapping attributes of its own. If a column name differs from the property name by more than case and underscores, declare a `RowMap<T>`.
24
24
@@ -46,10 +46,12 @@ The result is `PostRecord(1, 42, "Post 1", "Body 1")` and `PostRecord(2, 42, "Po
46
46
| The type | How a row is built |
47
47
|---|---|
48
48
| A class with a public parameterless constructor (it may have other constructors) |`new T()`, then its properties are set. The other constructors are never used |
49
-
| A struct with no public constructor, or with a public parameterless one |Its default value, then its properties are set. A struct's own parameterless constructor is not called|
49
+
| A struct with no public constructor, or with a public parameterless one |`new T()`, so a parameterless constructor you wrote runs; then its properties are set|
50
50
| A positional record, a record struct, or any class or struct whose only public constructor takes parameters | Through that constructor, then the properties it does not cover are set |
51
51
| A class or struct with two or more public constructors and none without parameters | Not mappable: `InvalidOperationException` at the call. Use `Map(selector)`, or make all but one constructor non-public |
52
52
53
+
A record is a class or struct like any other here: a record with a second public constructor is in the last row, unless the second one is parameterless, which puts it in the first. A constructor whose parameters all have default values still takes parameters, so it never counts as the parameterless one: next to another public constructor it is the last row too.
54
+
53
55
Only public constructors count. The choice depends on the type alone, never on the columns of a query. Use `Map(selector)` when you need to choose a constructor by the columns.
54
56
55
57
### How parameters are matched to columns
@@ -66,7 +68,7 @@ An `init` property is set like any other settable property. A `required` propert
66
68
67
69
-**A column must feed it, whatever the query** (a `required` property of a base class too). When the column names and types of a query include none for it, the first row throws `ValueMappingException`, with the property, its type, the model and the column that was looked for in the message. A query with no rows never reports it, except through the routes that know the columns before the first row: `DataTable.To<T>()`, and `ToChannel` and the Dataflow methods ([Channels and Dataflow](channels-and-dataflow.md)). (`Retrieve()` with `DequeueAs<T>()` did too; it is deprecated.)
68
70
-**A constructor parameter's default does not feed it.** A `required` property needs its own column, even when a constructor parameter has the same name.
69
-
-**A NULL in that column is not an error.** The column must be there, but a NULL cell leaves the property as it was: its initializer, else its default (`null` for a reference type), as for any other property.
71
+
-**A NULL in that column is not an error.** The column must be there, but a NULL cell follows [the NULL rules](#null-in-a-non-nullable-column), as for any other property.
70
72
-**It cannot be ignored.** A `RowMap<T>` pair with a `null` column for a `required` property throws `ArgumentException` when the map is created.
71
73
-**`[SetsRequiredMembers]`.** When the constructor the type is built through carries it, nothing is enforced: the members are ordinary properties.
72
74
-**A `required` public field** cannot be set, because fields are never mapped, so the type throws `InvalidOperationException` at the call. Declare it as a property.
@@ -128,7 +130,7 @@ The rules:
128
130
-**Which types.** A primitive, an enum, `string`, `decimal`, `DateTime`, `DateTimeOffset`, `TimeSpan`, `DateOnly`, `TimeOnly`, `Guid`, `byte[]` and a `Nullable<>` of those. Not `object`, `char[]`, a provider type such as `SqlInt32`, or a `Stream` (the reader is closed when the terminal returns): use `Map(selector)` for those.
129
131
-**The first column, whatever its name.** The other columns are ignored, so select only the one you need.
130
132
-**Converted like a property of that type.** When the column type matches, or is an integer that fits (a `long` into an `int` is range-checked; a `short` widens), the value is read with the typed getter and nothing is boxed. Otherwise it is converted: a `decimal` into an `int` rounds, text into a `Guid`, an enum from its name (ignoring case) or its number, a `DateTime` into a `DateTimeOffset`. A value that cannot be converted throws `ValueMappingException`. The reader is chosen once per result set, from the type the first row reports.
131
-
-**NULL.**A non-nullable value type gets `default` (`0`, `Guid.Empty`); `int?`, `string` and `byte[]` get `null`. Declare `int?` to tell NULL from 0.
133
+
-**NULL.**Follows [the NULL rules](#null-in-a-non-nullable-column): `Map<int>().Single()` over a NULL is `0`, and `Map<int?>()` gives `null`.
132
134
-**Read through.** As every `Map` terminal, they (and the async forms on the command) read through the later result sets; `command.SingleResult().Map<T>()` stops after the first.
133
135
134
136
It differs from the two other ways to read one column. `FirstOrdinalResults<T>()` returns the provider's own value (its typed getter, or a plain cast) from the first result set only, and is the form for a raw `IDbCommand` or a reader (see [the foundation](foundation.md#reader-and-command-helpers)); `ExecuteScalar<T>()` returns one value, converted the same way as the column of `Map<T>()` (see [which scalar form](expressive-commands.md#which-scalar-form)).
@@ -177,6 +179,8 @@ Declare the map once and reuse it. A map is immutable and safe to share between
@@ -204,6 +208,8 @@ Values are read with the provider's typed getters (`GetInt32`, `GetString`, ...)
204
208
205
209
A NULL never overwrites a member the C# type declares non-nullable. A value-type property (`int`, `bool`, `DateTime`, an enum, ...) keeps its initializer, else its default, and so does a reference-type property when nullable annotations are on: `string Name { get; set; } = "";` keeps `""`, and one with no initializer stays `null`. A constructor parameter of such a type gets its declared default value, else its default. `Nullable<T>` (`int?`), a reference declared nullable (`string?`, or `[AllowNull]` on a property), a reference type in code with nullable annotations off, and a member declared as a type parameter (`T Value`) get `null`; nothing throws. If NULL means something, declare the member nullable (`int?`, `string?`): that is the only way to tell NULL from 0 or "".
206
210
211
+
The same holds for a single value (see [One value per row](#one-value-per-row)): `Map<int>().Single()` over a NULL is `0` and does not throw, while `Map<int?>()` and `Map<string>()` give `null`.
212
+
207
213
The rule reads the nullable annotations the compiler stores in the assembly, so it needs `<Nullable>enable</Nullable>` (or `#nullable enable`) in the code that declares the model.
0 commit comments