Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docsite/CONTRIBUTING-DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Every push to `master` rebuilds and publishes the site (`.github/workflows/docs.
- **Articles:** add a markdown file to `docsite/articles/` and one `- href: file.md` line to `docsite/articles/toc.yml` (the menu title is the file's first heading).
- **Namespace and package overviews:** a markdown file with `uid: <namespace>` and `summary: *content` front matter. Put it in `Source/<Project>/docs/` next to the code, or in `docsite/api/overview/`; both are picked up without a config edit.
- **Diagrams:** a standalone `.svg` in `docsite/images/`, colors as CSS variables, no external references.
- **Side-by-side comparisons:** wrap two markdown code blocks in `<div class="side-by-side">` (one `<div>` per column, blank lines around the content, styled by `templates/custom/public/main.css`). Both regions need the same number of lines so the rows align.
- **README.md:** a pitch with absolute links; its code blocks are copied from sample regions.
- **Build locally:** `dotnet tool restore`, then `dotnet docfx docsite/docfx.json --serve`.
- **Failures:** warnings are listed in the workflow summary and never block a deploy. A DocFX error or a sample that does not compile does, and the live site stays as it was.
241 changes: 213 additions & 28 deletions docsite/articles/coming-from-dapper.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,238 @@
# Coming from Dapper

Open.Database.Extensions is a set of extensions on ADO.NET, not a Dapper clone, and plenty of code uses both on the same connection. This page is for readers who know Dapper's API and want the equivalent here. For speed and memory, see [Performance](performance.md).
Open.Database.Extensions is a set of extensions on ADO.NET, not a Dapper clone, and plenty of code uses both. This page is for readers who know Dapper. The biggest difference is not the query syntax: it is who handles the connection. For speed and memory, see [Performance](performance.md).

## Side by side
## The steps, side by side

Both snippets run against the same data in the build, and the results are checked to be equal.
Each comparison lists the same four steps in the same order, row against row: (1) get the connection, (2) the command or query, (3) the parameters, (4) the terminal and the mapping. A row that one side does not need says "nothing". The SQL is predefined once, so the lines stay short:

**A list**
[!code-csharp[](../samples/ConnectionPattern.cs#shared_sql)]

[!code-csharp[](../samples/DapperComparison.cs#dapper_list)]
**Dapper needs no setup.** It needs the connection string and the provider type in scope at every call site. It opens a closed connection for the call and closes it again afterwards, so `connection.Open()` is not required; add it only if you want to control when the connection opens.

[!code-csharp[](../samples/DapperComparison.cs#ode_list)]
**Open.Database.Extensions needs a connection factory,** created or registered once per application (see [Registering the factory once](#registering-the-factory-once)):

**The first row**
[!code-csharp[](../samples/ConnectionPattern.cs#ode_setup_once)]

[!code-csharp[](../samples/DapperComparison.cs#dapper_first)]
After that, no call site writes any connection code. The counts below do not include this one line of one-time setup.

[!code-csharp[](../samples/DapperComparison.cs#ode_first)]
### A list of rows

**A scalar**
<div class="side-by-side">
<div>

[!code-csharp[](../samples/DapperComparison.cs#dapper_scalar)]
**Dapper**

[!code-csharp[](../samples/DapperComparison.cs#ode_scalar)]
[!code-csharp[](../samples/ConnectionPattern.cs#dapper_list_call)]

**A command with no result set**
</div>
<div>

[!code-csharp[](../samples/DapperComparison.cs#dapper_execute)]
**Open.Database.Extensions (factory)**

[!code-csharp[](../samples/DapperComparison.cs#ode_execute)]
[!code-csharp[](../samples/ConnectionPattern.cs#ode_list_call)]

## Differences to know about
</div>
</div>

| | Dapper | Open.Database.Extensions |
|---|---|---|
| Parameters | An anonymous object or `DynamicParameters` | `.AddParam(name, value)` calls on the command |
| Starting point | Extension methods on `IDbConnection` | `connection.Command(sql)` or `connectionFactory.Command(sql)` |
| Connection handling | Opens a closed connection for the call and closes it again | Same rule for a connection. From a factory, it creates and disposes the connection |
| Mapping to a class | Constructor binding, settable properties and fields, configurable name matching | Public settable properties only; see [the rules](mapping.md#the-rules) |
| Positional records | Supported | Not supported: `Map<T>()` needs `new T()` |
| Other mapping features | Multi-mapping, `QueryMultiple`, dynamic results, custom type handlers | Not provided; use `Map(selector)` or the reader helpers |
Lines you write per call: Dapper 4, Open.Database.Extensions 3 (plus the one-time setup).

Dapper's features are described in Dapper's own documentation. The record example below runs in the build against Dapper 2.1.89:
What is different:

- **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.
- **Rows.** Dapper's `Query<T>` buffers by default (`buffered = true`) and returns an `IEnumerable<T>`, turned into a list here by `AsList()`. `ToList()` reads every row into a list during the call. `Read(handler)` offers a lazy sequence that is only valid inside the handler.
- **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 only and do no naming-convention matching: a renamed column needs an override (see [Renamed columns](#renamed-columns)).
- **Null parameters.** Both convert a null parameter value to `DBNull`.
- **Mapping errors.** Dapper wraps them in a `DataException` ("Error parsing column ..."). The extensions throw an `InvalidOperationException` that names the property and the column.

### One row

<div class="side-by-side">
<div>

**Dapper**

[!code-csharp[](../samples/ConnectionPattern.cs#dapper_first_call)]

</div>
<div>

**Open.Database.Extensions (factory)**

[!code-csharp[](../samples/ConnectionPattern.cs#ode_first_call)]

</div>
</div>

Lines you write per call: Dapper 3, Open.Database.Extensions 3 (plus the one-time setup).

What is different:

- **No rows, or several.** Dapper's `QueryFirstOrDefault` returns `default` for no rows and the first row for several. `QuerySingleOrDefault` returns `default` for none and throws for more than one. `QueryFirst` and `QuerySingle` throw when there is no row. The extensions' `FirstOrDefault()` returns `null` for no rows and the first row for several; `SingleOrDefault()` returns `null` for none and throws `InvalidOperationException` for more than one. There is no throwing `First` or `Single`.

### A scalar

<div class="side-by-side">
<div>

**Dapper**

[!code-csharp[](../samples/ConnectionPattern.cs#dapper_scalar_call)]

</div>
<div>

**Open.Database.Extensions (factory)**

[!code-csharp[](../samples/ConnectionPattern.cs#ode_scalar_call)]

</div>
</div>

Lines you write per call: Dapper 3, Open.Database.Extensions 3 (plus the one-time setup).

What is different:

- **The value's type.** Dapper converts the provider's value to `T` where it can (it uses `Convert.ChangeType`) and returns `default` for `NULL`. The extensions' `ExecuteScalar<T>()` casts the value exactly as the provider returns it, so `T` must be that type (SQLite counts as `long`, SQL Server as `int`). To convert, pass a function: `ExecuteScalar(value => ...)`.

### A command with no result set

<div class="side-by-side">
<div>

**Dapper**

[!code-csharp[](../samples/ConnectionPattern.cs#dapper_nonquery_call)]

</div>
<div>

**Open.Database.Extensions (factory)**

[!code-csharp[](../samples/ConnectionPattern.cs#ode_nonquery_call)]

</div>
</div>

Lines you write per call: Dapper 3, Open.Database.Extensions 3 (plus the one-time setup).

What is different:

- **The result.** Both return the number of rows affected, as an `int`.

### A transaction

Two facts are both true, and mixing them up causes the "Dapper needs `Open()`" belief. Dapper opens a closed connection for plain queries and closes it again afterwards. ADO.NET requires an open connection to begin a transaction, so any code that begins one opens the connection first. Dapper does not begin, commit or roll back transactions: its methods take an `IDbTransaction` that you created from an open connection.

<div class="side-by-side">
<div>

**Dapper**

[!code-csharp[](../samples/ConnectionPattern.cs#dapper_transaction_call)]

</div>
<div>

**Open.Database.Extensions (factory)**

[!code-csharp[](../samples/ConnectionPattern.cs#ode_transaction_call)]

</div>
</div>

Lines you write per call: Dapper 14, Open.Database.Extensions 6 (plus the one-time setup).

What is different:

- **Who opens the connection.** Dapper: you, with `Open()`, because `BeginTransaction()` needs it. Microsoft's `SqlConnection.BeginTransaction` examples open the connection first, and Microsoft.Data.Sqlite reports that the call "can only be called when the connection is open". The extensions' `ExecuteTransaction` opens the connection if it is closed.
- **Begin, commit, roll back.** Dapper: you write all three, usually with a `try`/`catch`, and pass the transaction to every call. The extensions begin the transaction, run your action, commit when it returns without throwing, and roll back (and rethrow) when it throws. `ExecuteTransactionConditional` commits only when your action returns `true`; otherwise it rolls back.
- **A connection that was already open.** The extensions close only a connection they opened; an open connection stays open.
- **Where the helpers live.** They are extension methods on `DbConnection`, with async forms (`ExecuteTransactionAsync`, `ExecuteTransactionConditionalAsync`), an optional isolation level and a cancellation token. A factory reaches them through `Using`.
### A connection you own

If you would rather own the connection, the extensions work on a connection the way Dapper does: the same first row, then the same rows as the factory form.

<div class="side-by-side">
<div>

**Dapper**

[!code-csharp[](../samples/ConnectionPattern.cs#dapper_list_call)]

</div>
<div>

**Open.Database.Extensions (your connection)**

[!code-csharp[](../samples/ConnectionPattern.cs#ode_list_own)]

</div>
</div>

Lines you write per call: Dapper 4, Open.Database.Extensions 4.

What is different:

- **Nothing about the connection.** You create it and dispose it in both. Each opens a closed connection for the call and closes only what it opened; neither needs `Open()`. The extensions never dispose a connection you gave them.
- **Concurrency.** A command built from one connection throws `InvalidOperationException` if two terminals run at once.

### The total

Across the four query calls above Dapper takes 13 lines and the extensions 12, plus one line of one-time setup. The counts are close: the difference is what the lines are. None of the extensions' per-call lines creates, owns or disposes a connection, so there is nothing to forget, to leak or to keep open too long, and connection handling lives in one place instead of at every call site.

## Who owns what

| | Dapper | Extensions with a factory | Extensions with your connection |
|---|---|---|---|
| Creates the connection | You, at each call site | The factory, for each execution | You |
| Needs the connection string and provider type in scope | At each call site | Once, where the factory is created | You, where you create it |
| Opens it | Dapper, if it is closed | The command, if it is closed | The command, only if it is closed |
| Closes it | Dapper, if it opened it | The command disposes it | The command, only if it opened it |
| Disposes it | You | The command | You. Never the command |

## Registering the factory once

`IDbConnectionFactory<TConnection>` is the abstraction, `DbConnectionFactory<TConnection>` wraps any `Func<TConnection>`, and `SqlConnectionFactory` (in the MSSqlClient package) takes a connection string. The library has no dependency-injection extension of its own: register the factory with whatever container you use. With Microsoft.Extensions.DependencyInjection:

[!code-csharp[](../samples/ConnectionPattern.cs#di_registration)]

Consumers take the factory as a constructor parameter and never see a connection string:

[!code-csharp[](../samples/ConnectionPattern.cs#di_consumer)]

For SQL Server, register `SqlConnectionFactory` as `IDbConnectionFactory<SqlConnection>`:

[!code-csharp[](../samples/ConnectionPattern.cs#sql_server_registration)]

A singleton is fine because the factory holds no connection: `Create()` returns a new connection object each time.

## Renamed columns

Dapper can match underscored names (`MatchNamesWithUnderscores`) or you can alias the columns in SQL. With the extensions, name the column for each property that needs it. `nameof` keeps the property side checked by the compiler:

[!code-csharp[](../samples/ConnectionPattern.cs#ode_renamed_columns)]

## Async and cancellation

Dapper's `QueryAsync<T>` returns a `Task<IEnumerable<T>>`, and a cancellation token travels in a `CommandDefinition`. The extensions' terminals have `Async` twins that take a `CancellationToken` and return a `ValueTask`, for example `ToArrayAsync(cancellationToken)`; a token set on the command with `UseCancellationToken(token)` is used when a terminal is not given one.

## Where Dapper does more

Dapper has features this library does not offer:

- **Constructor binding.** Positional records work with Dapper. `Map<T>()` needs `new T()` and settable properties.
- **Multi-mapping and `QueryMultiple`.** Dapper maps several types from one row and reads several result sets from one call.
- **Type handlers and custom type maps.** `SqlMapper.AddTypeHandler` and `SqlMapper.SetTypeMap`.
- **Dynamic results.** `Query(sql)` without a type returns dynamic rows.

[!code-csharp[](../samples/DapperComparison.cs#dapper_record)]

With the extensions, use `Map(selector)` or the reader helpers for those jobs.

## Migrating a call site

1. Replace the anonymous parameter object with `.AddParam` calls.
2. Replace `Query<T>(sql, ...)` with `Command(sql)...Map<T>().ToList()` (or `ToArray()`).
1. Create the connection factory once and register or inject it.
2. Replace `using var connection = ...` and `connection.Query<T>(sql, new { ... })` with `connectionFactory.Command(sql).AddParam(...).Map<T>().ToList()` (or `ToArray()`).
3. If a model uses constructor binding, give it a parameterless constructor and settable properties, or write a `Map(selector)`.
4. If column names differ from property names, add overrides: Dapper's global name matching has no equivalent here.
4. If column names differ from property names, add overrides.
19 changes: 14 additions & 5 deletions docsite/articles/expressive-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@ An expressive command is a small fluent object: pick the SQL (or stored procedur

## Two starting points

The factory form and the connection form look the same after the first call.
The factory form (`connectionFactory.Command(sql)` or `connectionFactory.StoredProcedure(name)`) and the connection form (`connection.Command(sql)` or `connection.StoredProcedure(name)`) look the same after the first call. Side by side, row against row:

**Factory form** (`connectionFactory.Command(sql)` or `connectionFactory.StoredProcedure(name)`):
<div class="side-by-side">
<div>

[!code-csharp[](../samples/ExpressiveCommands.cs#factory_form)]
**Factory form**

**Connection form** (`connection.Command(sql)` or `connection.StoredProcedure(name)`):
[!code-csharp[](../samples/ConnectionPattern.cs#ode_list_call)]

[!code-csharp[](../samples/ExpressiveCommands.cs#connection_form)]
</div>
<div>

**Connection form**

[!code-csharp[](../samples/ConnectionPattern.cs#ode_list_own)]

</div>
</div>

In the factory form the command creates, opens and disposes a connection for every terminal. In the connection form the connection is yours: the command opens it only when it is closed, closes only what it opened, and never disposes it. See [who owns the connection](foundation.md#who-owns-the-connection).

Expand Down
56 changes: 51 additions & 5 deletions docsite/articles/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,61 @@ A model needs a public parameterless constructor and public settable properties.

[!code-csharp[](../samples/Models.cs#post_model)]

There are two ways to start a command. Both end the same way, with `AddParam` for parameters and a terminal that runs the query.
There are two ways to start a command. Both end the same way, with `AddParam` for parameters and a terminal that runs the query. Side by side, row against row ("nothing" marks a step the factory makes unnecessary):

**From a connection factory.** The command takes a connection from the factory when a terminal runs, opens it, and disposes it afterwards:
<div class="side-by-side">
<div>

[!code-csharp[](../samples/ExpressiveCommands.cs#factory_form)]
**From a connection factory**

**From a connection you already have.** You own the connection. The command opens it only if it is closed and closes only what it opened:
[!code-csharp[](../samples/ConnectionPattern.cs#ode_list_call)]

[!code-csharp[](../samples/ExpressiveCommands.cs#connection_form)]
</div>
<div>

**From a connection you own**

[!code-csharp[](../samples/ConnectionPattern.cs#ode_list_own)]

</div>
</div>

The factory is created once per application:

[!code-csharp[](../samples/ConnectionPattern.cs#ode_setup_once)]

What is different:

- **Who creates the connection.** The factory, for each execution; or you.
- **Open and close.** In both forms the command opens the connection only if it is closed. The factory form disposes the connection when the terminal returns. In the connection form the command closes only what it opened and never disposes it.

## Before and after

The same query with plain ADO.NET and with the extensions, row against row: the connection, the command, the parameter, then the read loop and the mapping.

<div class="side-by-side">
<div>

**Plain ADO.NET**

[!code-csharp[](../samples/ConnectionPattern.cs#plain_list_aligned)]

</div>
<div>

**Open.Database.Extensions (factory)**

[!code-csharp[](../samples/ConnectionPattern.cs#ode_list_aligned)]

</div>
</div>

Lines you write per call: plain ADO.NET 20, Open.Database.Extensions 4 (plus one line of one-time setup per application).

What is different:

- **Plain ADO.NET** needs you to open the connection, create and dispose the command and the reader, add each parameter by hand and copy each column by ordinal, with an `IsDBNull` check for nullable columns.
- **With the extensions** the factory opens and disposes the connection, the command is one call, and the terminal does the read loop and the mapping. Matching columns to properties is by name, ignoring case.

## With SQL Server

Expand Down
2 changes: 1 addition & 1 deletion docsite/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
}
],
"output": "_site",
"template": [ "default", "modern" ],
"template": [ "default", "modern", "templates/custom" ],
"globalMetadata": {
"_appName": "Open.Database.Extensions",
"_appTitle": "Open.Database.Extensions",
Expand Down
Loading