Donald 8.0.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package Donald --version 8.0.2                
NuGet\Install-Package Donald -Version 8.0.2                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Donald" Version="8.0.2" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Donald --version 8.0.2                
#r "nuget: Donald, 8.0.2"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Donald as a Cake Addin
#addin nuget:?package=Donald&version=8.0.2

// Install Donald as a Cake Tool
#tool nuget:?package=Donald&version=8.0.2                

Donald

NuGet Version build

Meet Donald.

If you're a programmer and have used a database, he's impacted your life in a big way.

This library is named after him.

Honorable mention goes to @dsyme another important Donald and F#'s BDFL.

Key Features

Donald is a well-tested library that aims to make working with ADO.NET safer and a lot more succinct. It is an entirely generic abstraction, and will work with all ADO.NET implementations.

If you came looking for an ORM (object-relational mapper), this is not the library for you. And may the force be with you.

Design Goals

  • Support all ADO implementations.
  • Provide a succinct API for interacting with databases.
  • Enable asynchronuos workflows.
  • Provide explicit error flow control.
  • Make object mapping easier.
  • Improve data access performance.

Getting Started

Install the Donald NuGet package:

PM>  Install-Package Donald

Or using the dotnet CLI

dotnet add package Donald

Quick Start

open Donald

type Author = { FullName : string }

module Author =
  let ofDataReader (rd : IDataReader) : Author =      
      { FullName = rd.ReadString "full_name" }

let authors : Result<Author list, DbError> =    
    let sql = "
    SELECT  full_name 
    FROM    author 
    WHERE   author_id = @author_id"

    let param = [ "author_id", SqlType.Int 1 ]

    use conn = new SQLiteConnection "{your connection string}"
    
    conn
    |> Db.newCommand sql
    |> Db.setParams param    
    |> Db.query Author.ofDataReader

An Example using SQLite

For this example, assume we have an IDbConnection named conn:

Reminder: Donald will work with any ADO implementation (SQL Server, SQLite, MySQL, Postgresql etc.).

Consider the following model:

type Author = 
    { AuthorId : int
      FullName : string }

module Author -
    let ofDataReader (rd : IDataReader) : Author =         
        { AuthorId = rd.ReadInt32 "author_id"
          FullName = rd.ReadString "full_name" }

Query for multiple strongly-typed results

Important: Donald is set to use CommandBehavior.SequentialAccess by default. See performance for more information.

let sql = "SELECT author_id, full_name FROM author"

// Fluent
conn
|> Db.newCommand sql
|> Db.query Author.ofDataReader // Result<Author list, DbError>

// Expression
dbCommand conn {
    cmdText sql
}
|> Db.query Author.ofDataReader // Result<Author list, DbError>

// Async
conn
|> Db.newCommand sql
|> Db.Async.query Author.ofDataReader // Task<Result<Author list, DbError>>

Query for a single strongly-typed result

let sql = "SELECT author_id, full_name FROM author"
// Fluent
conn
|> Db.newCommand sql
|> Db.setParams [ "author_id", SqlType.Int 1 ]
|> Db.querySingle Author.ofDataReader // Result<Author option, DbError>

// Expression
dbCommand conn {
    cmdText sql
    cmdParam [ "author_id", SqlType.Int 1]
} 
|> Db.querySingle Author.ofDataReader // Result<Author option, DbError>

// Async
conn
|> Db.newCommand sql
|> Db.setParams [ "author_id", SqlType.Int 1 ]
|> Db.Async.querySingle Author.ofDataReader // Task<Result<Author option, DbError>>

Execute a statement

let sql = "INSERT INTO author (full_name)"

let param = [ "full_name", SqlType.String "John Doe" ]

// Fluent
conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.exec // Result<unit, DbError>

// Expression 
dbCommand conn {
    cmdText sql
    cmdParam param
}
|> Db.exec // Result<unit, DbError>

// Async
conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.Async.exec // Task<Result<unit, DbError>>

Execute a statement many times

let sql = "INSERT INTO author (full_name)" 

let param = 
    [ "full_name", SqlType.String "John Doe"
      "full_name", SqlType.String "Jane Doe" ]

// Fluent
conn
|> Db.newCommand sql
|> Db.execMany param

// Expression
dbCommand conn {
   cmdText sql
}
|> Db.execMany param

// Async
conn
|> Db.newCommand sql
|> Db.Async.execMany param
let sql = "INSERT INTO author (full_name)"

let param = [ "full_name", SqlType.String "John Doe" ]
// Fluent
conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.exec // Result<unit, DbError>

// Expression 
dbCommand conn {
    cmdText sql
    cmdParam param
}
|> Db.exec // Result<unit, DbError>

// Async
conn
|> Db.newCommand sql
|> Db.setParams param
|> Db.Async.exec // Task<Result<unit, DbError>>

Execute statements within an explicit transaction

Donald exposes most of it's functionality through dbCommand { ... } and the Db module. But three IDbTransaction type extension are exposed to make dealing with transactions safer:

  • TryBeginTransaction() opens a new transaction or raises CouldNotBeginTransactionError
  • TryCommit() commits a transaction or raises CouldNotCommitTransactionError and rolls back
  • TryRollback() rolls back a transaction or raises CouldNotRollbackTransactionError
// Safely begin transaction or throw CouldNotBeginTransactionError on failure
use tran = conn.TryBeginTransaction()

let insertSql = "INSERT INTO author (full_name)"

let param = [ "full_name", SqlType.String "John Doe" ]

let insertCmd = dbCommand conn {
    cmdText insertSql
    cmdParam param
    cmdTran  tran
}

let selectSql = "SELECT author_id, full_name FROM author WHERE full_name = @full_name"

let selectCmd = dbCommand conn {
    cmdText selectSql
    cmdParam param
    cmdTran  tran
} 

// Execute commands
let result = dbResult {
  do! insertCmd |> Db.exec 
  return! selectCmd |> Db.querySingle Author.ofDataReader
}

// Attempt to commit, rollback on failure and throw CouldNotCommitTransactionError
tran.TryCommit() 

Command Builder

At the core of Donald is a computation expression for building DbUnit instances, which encapsulate IDbCommand allowing additional properties to be configured prior to execution. It exposes five modification points:

  1. cmdText - SQL statement you intend to execute (default: String.empty).
  2. cmdParam - Input parameters for your statement (default: []).
  3. cmdType - Type of command you want to execute (default: CommandType.Text)
  4. cmdTran - Transaction to assign to command.
  5. cmdTimeout - The maximum time a command can run for (default: underlying DbCommand default, usually 30 seconds)
  6. cmdBehavior - The CommandBehavior setting for the DbUnit (default: CommandBehavior.SequentialAccess).

Reading Values

To make obtaining values from reader more straight-forward, 2 sets of extension methods are available for:

  1. Get value, automatically defaulted
  2. Get value as option<'a>

If you need an explicit Nullable<'a> you can use Option.asNullable.

Assuming we have an active IDataReader called rd and are currently reading a row, the following extension methods are available to simplify reading values:

rd.ReadString "some_field"         // string -> string
rd.ReadBoolean "some_field"        // string -> bool
rd.ReadByte "some_field"           // string -> byte
rd.ReadChar "some_field"           // string -> char
rd.ReadDateTime "some_field"       // string -> DateTime
rd.ReadDecimal "some_field"        // string -> Decimal
rd.ReadDouble "some_field"         // string -> Double
rd.ReadFloat "some_field"          // string -> float32
rd.ReadGuid "some_field"           // string -> Guid
rd.ReadInt16 "some_field"          // string -> int16
rd.ReadInt32 "some_field"          // string -> int32
rd.ReadInt64 "some_field"          // string -> int64
rd.ReadBytes "some_field"          // string -> byte[]

rd.ReadStringOption "some_field"   // string -> string option
rd.ReadBooleanOption "some_field"  // string -> bool option
rd.ReadByteOption "some_field"     // string -> byte option
rd.ReadCharOption "some_field"     // string -> char option
rd.ReadDateTimeOption "some_field" // string -> DateTime option
rd.ReadDecimalOption "some_field"  // string -> Decimal option
rd.ReadDoubleOption "some_field"   // string -> Double option
rd.ReadFloatOption "some_field"    // string -> float32 option
rd.ReadGuidOption "some_field"     // string -> Guid option
rd.ReadInt16Option "some_field"    // string -> int16 option
rd.ReadInt32Option "some_field"    // string -> int32 option
rd.ReadInt64Option "some_field"    // string -> int64 option
rd.ReadBytesOption "some_field"    // string -> byte[] option

Exceptions

Donald exposes DbError type to represent failure at different points in the execution-cycle, all of which are encapsulated within a general DbFailureException.

type DbError =
    | DbConnectionError of DbConnectionError
    | DbTransactionError of DbTransactionError
    | DbExecutionError of DbExecutionError
    | DataReaderCastError of DataReaderCastError
    | DataReaderOutOfRangeError of DataReaderOutOfRangeError

exception DbFailureException of DbError    

During command execution failures the Error case of Result contains one of DbError union cases with relevant data.

/// Details of failure to connection to a database/server.
type DbConnectionError =
    { ConnectionString : string
      Error : exn }

/// Details the steps of database a transaction.
type DbTransactionStep =  TxBegin | TxCommit | TxRollback

/// Details of transaction failure.
type DbTransactionError =
    { Step : DbTransactionStep
      Error : exn }

/// Details of failure to execute database command.
type DbExecutionError =
    { Statement : string
      Error : DbException }

/// Details of failure to cast a IDataRecord field.
type DataReaderCastError =
    { FieldName : string
      Error : InvalidCastException }

/// Details of failure to access a IDataRecord column by name.
type DataReaderOutOfRangeError =
    { FieldName : string
      Error : IndexOutOfRangeException }

It's important to note that Donald will only raise exceptions in exceptional situations.

Performance

By default, Donald will consume IDataReader using CommandBehavior.SequentialAccess. This allows the rows and columns to be read in chunks (i.e., streamed), but forward-only. As opposed to being completely read into memory all at once, and readable in any direction. The benefits of this are particular felt when reading large CLOB (string) and BLOB (binary) data. But is also a measureable performance gain for standard query results as well.

The only nuance to sequential access is that columns must be read in the same order found in the SELECT clause. Aside from that, there is no noticeable difference from the perspective of a library consumer.

Configuring CommandBehavior can be done two ways:

let sql = "SELECT author_id, full_name FROM author"
// Fluent 
conn
|> Db.newCommand sql
|> Db.setCommandBehavior CommandBehavior.Default
|> Db.query Author.ofDataReader

// Expression
dbCommand conn {
    cmdText sql
    cmdBehavior CommandBehavior.Default
}
|> Db.query Author.ofDataReader

Find a bug?

There's an issue for that.

License

Built with ♥ by Pim Brouwers in Toronto, ON. Licensed under Apache License 2.0.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
10.1.0 2,205 3/20/2024
10.0.2 1,097 12/12/2023
10.0.1 967 7/11/2023
10.0.0 152 7/8/2023
10.0.0-alpha3 183 2/11/2023
10.0.0-alpha2 159 2/4/2023
10.0.0-alpha1 145 2/3/2023
9.0.1 1,334 1/11/2023
9.0.0 315 12/23/2022
8.0.2 15,281 11/23/2022
8.0.1 323 11/23/2022
8.0.0 392 11/23/2022
7.1.0 2,706 12/17/2021
7.0.0 332 12/14/2021
7.0.0-alpha1 273 11/1/2021
6.2.5 631 8/4/2021
6.2.4 383 8/4/2021
6.2.3 390 7/30/2021
6.2.2 466 7/27/2021
6.2.1 358 7/27/2021
6.2.0 573 7/26/2021
6.1.0 473 7/6/2021
6.1.0-beta3 234 7/5/2021
6.1.0-beta2 244 7/4/2021
6.1.0-beta1 348 7/4/2021
6.0.0 425 4/11/2021
5.1.3 422 3/29/2021
5.1.2 447 2/27/2021
5.1.1 458 1/23/2021
5.0.1 1,046 12/3/2020
5.0.0 432 12/1/2020
5.0.0-alpha3 291 12/1/2020
5.0.0-alpha2 268 11/30/2020
5.0.0-alpha1 272 11/30/2020
4.0.0 488 11/12/2020
3.0.4 1,759 10/31/2020
3.0.3 743 8/2/2020
3.0.2 530 7/17/2020
3.0.1 514 7/14/2020
3.0.0 554 6/29/2020
2.0.2 485 5/1/2020
2.0.1 454 4/27/2020
2.0.0 463 4/27/2020
1.0.6 438 4/24/2020
1.0.4 422 4/24/2020
1.0.3 452 4/24/2020
1.0.2 454 4/24/2020
1.0.1 658 4/18/2020
1.0.0 490 4/5/2020