Build DuckDB extensions with C#
developDuckDB.ExtensionKit repository.repository·develop·Indexed 20 days ago
https://github.com/giorgi/duckdb.netC# bindings for the DuckDB analytical database providing an ADO.NET compatible interface. It enables executing SQL queries via DuckDBConnection and offers a type-safe DuckDBAppenderMap<T> pattern to validate .NET types against the database schema to prevent data corruption. Supports connections to local database files and MotherDuck.
DuckDB.ExtensionKit repository.To prevent silent data corruption caused by type mismatches (e.g., appending a decimal to a REAL column), use the DuckDBAppenderMap<T> pattern. This approach validates your .NET types against the actual database schema at the moment the appender is created.
Create a class inheriting from DuckDBAppenderMap<T>. You must define property mappings in the exact order they appear in the database table columns.
public class PersonMap : DuckDBAppenderMap<Person>
{
public PersonMap()
{
Map(p => p.Id); // Column 0
Map(p => p.Name); // Column 1
Map(p => p.Height); // Column 2
Map(p => p.BirthDate); // Column 3
}
}Use the CreateAppender<T, TMap> method on your connection. If the types in your map do not match the database schema, an exception will be thrown immediately upon creation.
// Create table
connection.ExecuteNonQuery("CREATE TABLE person(id INTEGER, name VARCHAR, height REAL, birth_date TIMESTAMP)");
var people = new[]
{
new Person { Id = 1, Name = "Alice", Height = 1.65f, BirthDate = new DateTime(1990, 1, 15) }
};
// Type validation happens here at creation
using (var appender = connection.CreateAppender<Person, PersonMap>("person"))
{
appender.AppendRecords(people);
}public class PersonMap : DuckDBAppenderMap<Person>
{
public PersonMap()
{
Map(p => p.Id);
Map(p => p.Name);
Map(p => p.Height);
Map(p => p.BirthDate);
}
}
using (var appender = connection.CreateAppender<Person, PersonMap>("person"))
{
appender.AppendRecords(people);
}To connect to MotherDuck, use a connection string starting with md: and include your database name and motherduck_token.
using var duckDBConnection = new DuckDBConnection("DataSource=md:{your_database}?motherduck_token=ey...");You can install DuckDB.NET via NuGet. The DuckDB.NET.Data.Full package is recommended for most users as it includes the necessary native dependencies.
Use the following command to add the full package to your project:
dotnet add package DuckDB.NET.Data.FullIf you encounter System.AccessViolationException: Attempted to read or write protected memory while debugging, this is typically caused by the debugger interacting with native memory during marshalling.
Workaround: Check the JetBrains Rider issue regarding debugger options messing up debugging sessions during marshalling for specific configuration steps to resolve this.
DuckDB.NET provides an ADO.NET compatible interface. You can use DuckDBConnection to connect to a database file, create commands, and execute queries using standard methods like ExecuteNonQuery, ExecuteScalar, and ExecuteReader.
using (var duckDBConnection = new DuckDBConnection("Data Source=file.db"))
{
duckDBConnection.Open();
using var command = duckDBConnection.CreateCommand();
command.CommandText = "CREATE TABLE integers(foo INTEGER, bar INTEGER);";
var executeNonQuery = command.ExecuteNonQuery();
command.CommandText = "INSERT INTO integers VALUES (3, 4), (5, 6), (7, 8);";
executeNonQuery = command.ExecuteNonQuery();
command.CommandText = "Select count(*) from integers";
var executeScalar = command.ExecuteScalar();
command.CommandText = "SELECT foo, bar FROM integers";
var reader = command.ExecuteReader();
PrintQueryResults(reader);
}
private static void PrintQueryResults(DbDataReader queryResult)
{
for (var index = 0; index < queryResult.FieldCount; index++)
{
var column = queryResult.GetName(index);
Console.Write($"{column} ");
}
Console.WriteLine();
while (queryResult.Read())
{
for (int ordinal = 0; ordinal < queryResult.FieldCount; ordinal++)
{
var val = queryResult.GetInt32(ordinal);
Console.Write(val);
Console.Write(" ");
}
Console.WriteLine();
}
}The CreateAppender<T, TMap> method supports different levels of database object scoping:
connection.CreateAppender<T, TMap>("tableName")connection.CreateAppender<T, TMap>("schemaName", "tableName")connection.CreateAppender<T, TMap>("catalog", "schema", "table")When defining a DuckDBAppenderMap<T>, you can control how specific columns are handled using the following methods within the constructor:
Map(expression): Maps a property to the next available column in the sequence.DefaultValue(): Instructs the appender to use the column's defined default value instead of a value from the object.NullValue(): Instructs the appender to insert a NULL value for that column.Example:
public class MyMap : DuckDBAppenderMap<MyData>
{
public MyMap()
{
Map(d => d.Id);
Map(d => d.Name);
DefaultValue(); // Use column's default value
NullValue(); // Insert NULL
}
}public class MyMap : DuckDBAppenderMap<MyData>
{
public MyMap()
{
Map(d => d.Id);
Map(d => d.Name);
DefaultValue();
NullValue();
}
}The AppenderMap validates .NET types against the following DuckDB types. If a mismatch occurs (e.g., using decimal for a REAL column), an exception is thrown during appender creation.
| .NET Type | DuckDB Type |
|---|---|
bool | Boolean |
sbyte | TinyInt |
short | SmallInt |
int | Integer |
long | BigInt |
byte | UnsignedTinyInt |
ushort | UnsignedSmallInt |
uint | UnsignedInteger |
ulong | UnsignedBigInt |
float | Float |
double | Double |
decimal | Decimal |
string | Varchar |
DateTime | Timestamp |
DateTimeOffset | TimestampTz |
TimeSpan | Interval |
Guid | Uuid |
DateOnly | Date |
TimeOnly | Time |