VB.NET Dapper – Micro ORM for Clean Data Access (2026)
Dapper is the fastest way to replace tedious SqliteCommand boilerplate in your VB.NET application with clean, strongly typed data access. In this guide I’ll show you how to set up Dapper via NuGet, map query results directly to classes, and perform all CRUD operations with minimal code. If you’ve been writing AddWithValue and ExecuteReader by hand, this is your next step.
Inhaltsverzeichnis
- 1 What is Dapper and why use it?
- 2 Installing Dapper via NuGet
- 3 Setting up the model class
- 4 Connecting and creating tables
- 5 Inserting data
- 6 Querying data
- 7 Updating and deleting data
- 8 Complete example: ContactRepository with Dapper
- 9 Transactions with Dapper
- 10 Async queries: keeping the UI responsive
- 11 Dapper vs. raw ADO.NET: side-by-side comparison
- 12 Common pitfalls and tips
- 13 FAQ
- 14 Wrapping up
What is Dapper and why use it?
Dapper is a lightweight „micro ORM“ for .NET, originally built by the Stack Overflow team to solve their own performance problems. It sits on top of IDbConnection and extends it with convenience methods like Query(Of T) and Execute(). Here’s why it matters:
- No boilerplate – no more manual
SqliteCommand,AddWithValue,ExecuteReader, and column-by-column mapping - Strongly typed – query results map directly to your VB.NET classes by matching property names to column names
- Fast – Dapper is nearly as fast as raw ADO.NET, much faster than Entity Framework for simple queries
- No magic – you write real SQL, so you always know exactly what hits the database
- Works with any database – SQLite, SQL Server, PostgreSQL, MySQL, anything that has an
IDbConnection
If you’ve followed the VB.NET SQLite article, you already know how to create a database and run queries with SqliteCommand. Dapper replaces that manual work while keeping full control over your SQL.
Installing Dapper via NuGet
Open the Package Manager Console in Visual Studio (Tools → NuGet Package Manager → Package Manager Console) and run:
Install-Package Dapper Install-Package Microsoft.Data.Sqlite
You need both packages: Dapper provides the extension methods, and Microsoft.Data.Sqlite provides the database connection. If you already have Microsoft.Data.Sqlite installed from a previous project, just add Dapper.
Alternatively, right-click your project in Solution Explorer → Manage NuGet Packages → search for Dapper → Install. Then repeat for Microsoft.Data.Sqlite if you haven’t already.
Setting up the model class
Before writing any queries, define a class whose properties match the column names in your table. Dapper maps columns to properties by name, case-insensitive:
Public Class Contact
Public Property Id As Long
Public Property Name As String
Public Property Email As String
Public Property Phone As String
Public Property CreatedAt As String
End Class
That’s all Dapper needs. No attributes, no base classes, no configuration files. If a column name doesn’t match a property name, you can use a SQL alias: SELECT first_name AS Name.
Connecting and creating tables
Dapper works on top of any IDbConnection. You still create the connection yourself. Use AppContext.BaseDirectory to build a reliable path, as explained in the Application Path article:
Imports Dapper
Imports Microsoft.Data.Sqlite
Dim dbPath As String = Path.Combine(AppContext.BaseDirectory, "myapp.db")
Dim connectionString As String = $"Data Source={dbPath}"
Using connection As New SqliteConnection(connectionString)
connection.Open()
connection.Execute("
CREATE TABLE IF NOT EXISTS Contacts (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name TEXT NOT NULL,
Email TEXT,
Phone TEXT,
CreatedAt TEXT DEFAULT CURRENT_TIMESTAMP
)")
End Using
Notice how connection.Execute() replaces the entire SqliteCommand / ExecuteNonQuery() pattern. Dapper opens the connection automatically if it’s closed, but I prefer opening explicitly for clarity.
Inserting data
With raw ADO.NET you’d create a command, add parameters one by one, and call ExecuteNonQuery(). With Dapper, you pass an anonymous object and it maps the properties to parameter names:
Using connection As New SqliteConnection(connectionString)
connection.Open()
connection.Execute(
"INSERT INTO Contacts (Name, Email, Phone) VALUES (@Name, @Email, @Phone)",
New With {
.Name = "John Doe",
.Email = "john@example.com",
.Phone = "+49 123 456789"
})
End Using
Dapper automatically creates parameterized queries from the anonymous object. There is no risk of SQL injection because the values are never concatenated into the SQL string.
Inserting multiple rows at once
Pass a list instead of a single object and Dapper will execute the statement once per item:
Dim contacts As New List(Of Contact) From {
New Contact With {.Name = "Alice", .Email = "alice@example.com"},
New Contact With {.Name = "Bob", .Email = "bob@example.com"},
New Contact With {.Name = "Clara", .Email = "clara@example.com"}
}
Using connection As New SqliteConnection(connectionString)
connection.Open()
connection.Execute(
"INSERT INTO Contacts (Name, Email) VALUES (@Name, @Email)",
contacts)
End Using
For large datasets (hundreds or thousands of rows), wrap this in a transaction for significantly better performance. More on that in the transactions section below.
Querying data
This is where Dapper really shines. Instead of looping through a DataReader and mapping each column manually, you call Query(Of T) and get a typed collection back:
Using connection As New SqliteConnection(connectionString)
connection.Open()
Dim contacts = connection.Query(Of Contact)(
"SELECT Id, Name, Email, Phone FROM Contacts ORDER BY Name")
For Each contact In contacts
Console.WriteLine($"[{contact.Id}] {contact.Name} - {contact.Email}")
Next
End Using
The result is an IEnumerable(Of Contact) that you can iterate, filter with LINQ, or bind directly to a DataGridView.
Querying with parameters
Pass parameters as an anonymous object, just like with Execute():
Using connection As New SqliteConnection(connectionString)
connection.Open()
Dim results = connection.Query(Of Contact)(
"SELECT Id, Name, Email FROM Contacts WHERE Name LIKE @Search",
New With {.Search = $"%{searchTerm}%"})
For Each contact In results
Console.WriteLine($"[{contact.Id}] {contact.Name}")
Next
End Using
Getting a single row
Use QueryFirstOrDefault(Of T) when you expect exactly one result (or nothing):
Using connection As New SqliteConnection(connectionString)
connection.Open()
Dim contact = connection.QueryFirstOrDefault(Of Contact)(
"SELECT Id, Name, Email, Phone FROM Contacts WHERE Id = @Id",
New With {.Id = 1})
If contact IsNot Nothing Then
Console.WriteLine($"{contact.Name} ({contact.Email})")
End If
End Using
Other variants: QueryFirst (throws if no rows), QuerySingle (throws if not exactly one row), QuerySingleOrDefault (throws if more than one row).
Scalar queries
For queries that return a single value, use ExecuteScalar(Of T):
Using connection As New SqliteConnection(connectionString)
connection.Open()
Dim count = connection.ExecuteScalar(Of Long)(
"SELECT COUNT(*) FROM Contacts")
Console.WriteLine($"{count} contacts in database")
End Using
Updating and deleting data
Updates and deletes follow the same pattern as inserts. Execute() returns the number of affected rows:
UPDATE
Using connection As New SqliteConnection(connectionString)
connection.Open()
Dim rowsAffected = connection.Execute(
"UPDATE Contacts SET Email = @Email WHERE Id = @Id",
New With {.Email = "new-email@example.com", .Id = 1})
Console.WriteLine($"{rowsAffected} row(s) updated.")
End Using
DELETE
Using connection As New SqliteConnection(connectionString)
connection.Open()
Dim rowsAffected = connection.Execute(
"DELETE FROM Contacts WHERE Id = @Id",
New With {.Id = 5})
Console.WriteLine($"{rowsAffected} row(s) deleted.")
End Using
Complete example: ContactRepository with Dapper
Here’s a complete repository class that wraps all CRUD operations. Compare this to the manual ADO.NET version in the SQLite article to see much code Dapper saves:
Imports Dapper
Imports Microsoft.Data.Sqlite
Public Class ContactRepository
Private ReadOnly _connectionString As String
Public Sub New(connectionString As String)
_connectionString = connectionString
End Sub
Public Sub InitializeDatabase()
Using connection As New SqliteConnection(_connectionString)
connection.Open()
connection.Execute("
CREATE TABLE IF NOT EXISTS Contacts (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name TEXT NOT NULL,
Email TEXT,
Phone TEXT,
CreatedAt TEXT DEFAULT CURRENT_TIMESTAMP
)")
End Using
End Sub
Public Function AddContact(name As String, email As String, phone As String) As Long
Using connection As New SqliteConnection(_connectionString)
connection.Open()
connection.Execute(
"INSERT INTO Contacts (Name, Email, Phone) VALUES (@Name, @Email, @Phone)",
New With {.Name = name, .Email = email, .Phone = phone})
Return connection.ExecuteScalar(Of Long)("SELECT last_insert_rowid()")
End Using
End Function
Public Function GetAllContacts() As List(Of Contact)
Using connection As New SqliteConnection(_connectionString)
connection.Open()
Return connection.Query(Of Contact)(
"SELECT Id, Name, Email, Phone FROM Contacts ORDER BY Name").AsList()
End Using
End Function
Public Function GetContactById(id As Long) As Contact
Using connection As New SqliteConnection(_connectionString)
connection.Open()
Return connection.QueryFirstOrDefault(Of Contact)(
"SELECT Id, Name, Email, Phone FROM Contacts WHERE Id = @Id",
New With {.Id = id})
End Using
End Function
Public Function UpdateContact(id As Long, name As String, email As String, phone As String) As Boolean
Using connection As New SqliteConnection(_connectionString)
connection.Open()
Dim rows = connection.Execute(
"UPDATE Contacts SET Name = @Name, Email = @Email, Phone = @Phone WHERE Id = @Id",
New With {.Name = name, .Email = email, .Phone = phone, .Id = id})
Return rows > 0
End Using
End Function
Public Function DeleteContact(id As Long) As Boolean
Using connection As New SqliteConnection(_connectionString)
connection.Open()
Dim rows = connection.Execute(
"DELETE FROM Contacts WHERE Id = @Id",
New With {.Id = id})
Return rows > 0
End Using
End Function
End Class
Usage is straightforward:
Dim repo As New ContactRepository($"Data Source={dbPath}")
repo.InitializeDatabase()
Dim newId = repo.AddContact("John Doe", "john@example.com", "+49 123 456789")
Console.WriteLine($"Created contact with ID {newId}")
Dim allContacts = repo.GetAllContacts()
For Each c In allContacts
Console.WriteLine($"[{c.Id}] {c.Name} - {c.Email}")
Next
Transactions with Dapper
For bulk operations, wrap them in a transaction. Without a transaction, SQLite commits after every single statement, which is extremely slow for hundreds of inserts:
Using connection As New SqliteConnection(connectionString)
connection.Open()
Using transaction = connection.BeginTransaction()
Dim contacts As New List(Of Object)
For i As Integer = 1 To 1000
contacts.Add(New With {
.Name = $"Contact {i}",
.Email = $"contact{i}@example.com"
})
Next
connection.Execute(
"INSERT INTO Contacts (Name, Email) VALUES (@Name, @Email)",
contacts,
transaction)
transaction.Commit()
End Using
End Using
The key difference to raw ADO.NET: you pass the transaction object as a parameter to Execute(). No need to set command.Transaction manually. And because Dapper loops through the list internally, you get transaction safety for all 1000 inserts with minimal code.
Async queries: keeping the UI responsive
Every Dapper method has an async counterpart. If you’re building a WinForms or WPF application, use them to keep the UI from freezing during database calls. For the fundamentals, check out the VB.NET Async/Await guide.
Private Async Function LoadContactsAsync() As Task(Of List(Of Contact))
Using connection As New SqliteConnection(connectionString)
Await connection.OpenAsync()
Dim contacts = Await connection.QueryAsync(Of Contact)(
"SELECT Id, Name, Email, Phone FROM Contacts ORDER BY Name")
Return contacts.AsList()
End Using
End Function
Available async methods: QueryAsync, QueryFirstOrDefaultAsync, ExecuteAsync, ExecuteScalarAsync. They work exactly like their synchronous counterparts, just with Await.
Dapper vs. raw ADO.NET: side-by-side comparison
Here’s the same „get all contacts“ operation written both ways. First, raw ADO.NET as shown in the SQLite article:
' Raw ADO.NET - 13 lines
Using connection As New SqliteConnection(connectionString)
connection.Open()
Using command As New SqliteCommand("SELECT Id, Name, Email, Phone FROM Contacts ORDER BY Name", connection)
Using reader = command.ExecuteReader()
While reader.Read()
Dim contact As New Contact With {
.Id = reader.GetInt64(0),
.Name = reader.GetString(1),
.Email = If(reader.IsDBNull(2), "", reader.GetString(2)),
.Phone = If(reader.IsDBNull(3), "", reader.GetString(3))
}
contacts.Add(contact)
End While
End Using
End Using
End Using
Now the same thing with Dapper:
' Dapper - 4 lines
Using connection As New SqliteConnection(connectionString)
connection.Open()
Dim contacts = connection.Query(Of Contact)(
"SELECT Id, Name, Email, Phone FROM Contacts ORDER BY Name").AsList()
End Using
Same result, same SQL, same performance. Dapper handles the reader loop, the null checks, and the property mapping automatically. The more complex your model, the bigger the difference.
Common pitfalls and tips
Property names must match column names
Dapper maps by name, case-insensitive. If your table has first_name but your class has FirstName, use a SQL alias:
Dim contacts = connection.Query(Of Contact)(
"SELECT first_name AS FirstName, last_name AS LastName FROM Contacts")
NULL handling
Dapper handles NULL columns automatically. If a column is NULL and the target property is a String, the property will be Nothing. If you want an empty string instead, initialize your properties with defaults:
Public Class Contact
Public Property Id As Long
Public Property Name As String = ""
Public Property Email As String = ""
Public Property Phone As String = ""
End Class
Don’t forget Using blocks
Dapper does not manage connection lifetimes. You still need Using blocks for every connection, exactly like with raw ADO.NET. An open connection locks the SQLite file.
Dapper and Dependency Injection
In larger applications, inject the connection string instead of hardcoding it. The ContactRepository class above already follows this pattern. If you’re using a DI container like Autofac, check the Dependency Injection guide.
FAQ
Dapper is a lightweight micro ORM that extends IDbConnection with methods like Query(Of T) and Execute(). It maps SQL results directly to VB.NET classes without the overhead of a full ORM like Entity Framework.
Yes. Dapper is nearly as fast as raw ADO.NET because it does minimal processing. Entity Framework adds change tracking, proxy generation, and query translation, which makes it slower for simple operations. For CRUD-heavy applications, Dapper is often the better choice.
Yes. Install both Dapper and Microsoft.Data.Sqlite via NuGet. Dapper works with any IDbConnection, so SqliteConnection works out of the box without additional configuration.
Dapper maps NULL columns to Nothing for reference types. If you prefer empty strings, initialize your class properties with default values like Public Property Email As String = "".
Yes. Every Dapper method has an async counterpart: QueryAsync, ExecuteAsync, ExecuteScalarAsync, etc. Use them with Await in WinForms or WPF applications to keep the UI responsive.
Wrapping up
Dapper eliminates the repetitive ADO.NET boilerplate while keeping you in full control of your SQL. Install the NuGet package, define a model class, and replace your manual SqliteCommand / DataReader code with Query(Of T) and Execute(). If you haven’t set up your database yet, start with the VB.NET SQLite guide first, then come back here to simplify your data access layer.