So far (Part 1, Part 2, Part 3), we’ve covered pretty much everything you need to know in order to use the ADO .NET connected layer for your day-to-day data access layer development needs. We’ve seen how to open a connection, run different types of queries, use transactions, and use prepared statements to parameterise our queries. We’ve also talked a bit about why ADO .NET is still useful today, and the general structure of ADO .NET data providers.
In practice, while ADO .NET is easy, useful and fast, it can also be quite tedious. You’ll find yourself having to write a lot of the same boilerplate over and over, and it’s quite common for different companies to have their own libraries with utility methods for some of the most repeated operations.
Some of the guys at Stack Exchange (the company behind Stack Overflow) have built one such library, called Dapper. It’s sometimes called a micro-ORM, but I prefer to distance it even more from the ORM idea and simply call it a thin layer over ADO .NET. As we shall see shortly, it simply provides extension methods over the usual ADO .NET connection objects.
You can learn more about Dapper from the following links:
In this article, we’ll see how to use Dapper to do the same things we’ve been doing so far with ADO .NET, and how it makes life easier.
I’ll be using a .NET Core console application for this one, although it should work as-is for .NET Framework. I’m using SQL Server as the database engine. The current version of Dapper at this time is 2.0.35.
A Musical Example
For this article, create a database called Music, and run the following script:
create table Song (
Id int not null primary key identity(1, 1),
Name nvarchar(50) not null,
Year int not null,
ArtistId int not null
);
create table Artist (
Id int not null primary key identity(1, 1),
Name nvarchar(50) not null,
);
insert into Artist (Name) values
('Caravan Palace'),
('Red Hot Chili Peppers'),
('The Cure');
insert into Song (Name, Year, ArtistId) values
('Dramophone', 2012, 1),
('Can''t Stop', 2002, 2),
('Just Like Heaven', 1987, 3);
We’ll use this as a playground to explore Dapper’s querying capabilities.
Opening a Connection
When using Dapper, there is no strange new way to open a connection. You simply open an ADO .NET connection exactly as we did in Part 1. So if you’re using SQL Server, follow three simple steps.
First, ensure you have a reference to System.Data.SqlClient
. If you’re using .NET Core, this means adding the System.Data.SqlClient NuGet package. Projects based on the older .NET Framework need only add a reference since this assembly is part of the framework.
Secondly, import this assembly with a using
statement:
using System.Data.SqlClient;
Finally, write the basic code to create and open an SqlConnection
. I’m using the async version since we’ve already covered this in Part 2.
const string connStr = @"Server=.\SQLEXPRESS;
Initial Catalog=Music; Integrated Security=True";
using (var conn = new SqlConnection(connStr))
{
await conn.OpenAsync();
Console.WriteLine("Connected!");
}
Querying for Row Data
Once we have an ADO .NET connection object, we can use Dapper’s extension methods to execute queries. First, install the Dapper NuGet package. Then, make sure you import Dapper via a using
statement:
using Dapper;
Finally, just use the QueryAsync()
extension method to execute a query and retrieve the results:
using (var conn = new SqlConnection(connStr))
{
await conn.OpenAsync();
const string sql = "select Name from Artist";
var names = await conn.QueryAsync<string>(sql);
foreach (var name in names)
Console.WriteLine(name);
}
Here’s the output for that:
Caravan Palace
Red Hot Chili Peppers
The Cure
“A-ha!” I hear you say, “It’s simple enough with strings. What if I want to retrieve a row and return it as a C# object/DTO?” Alright, so for Artist we have this class:
public class Artist
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString() => $"{Id}: {Name}";
}
Then, we make some small changes to the querying code to (a) retrieve the ID along with the name in the query, (b) have QueryAsync()
use a generic argument of type Artist
, and (c) rename all the variables so they make sense, given the updates.
using (var conn = new SqlConnection(connStr))
{
await conn.OpenAsync();
const string sql = "select Id, Name from Artist";
var artists = await conn.QueryAsync<Artist>(sql);
foreach (var artist in artists)
Console.WriteLine(artist);
}
Dapper is intelligent enough to automatically map the column names to the properties of the specified class. That’s right… the entire SqlDataReader
part of ADO .NET, incuding fetching and converting individual fields, is no longer necessary. Here’s the output:
1: Caravan Palace
2: Red Hot Chili Peppers
3: The Cure
Querying for Scalar Data
For single-value queries, you use ExecuteScalarAsync()
instead of QueryAsync()
:
using (var conn = new SqlConnection(connStr))
{
await conn.OpenAsync();
const string sql = "select Name from Song where Id = 2";
var songName = await conn.ExecuteScalarAsync<string>(sql);
Console.WriteLine(songName);
}
The output for this is:
Can't Stop
Executing Non Queries
For inserts, updates and deletes, just use ExecuteAsync()
:
using (var conn = new SqlConnection(connStr))
{
await conn.OpenAsync();
const string sql = @"insert into Song(Name, Year, ArtistId)
values('Snow', 2006, 2);";
int rowsAffected = await conn.ExecuteAsync(sql);
Console.WriteLine($"{rowsAffected} rows inserted.");
}
The following is the output when running this program. If you query the Songs table, you’ll find the new row in there.
1 rows inserted.
Prepared Statements
When you need to run queries based on user input, you need to use prepared statements and pass the inputs as parameters. With Dapper, you provide an anonymous object where the property names match the parameters in the query. Here’s a full example:
static async Task Main(string[] args)
{
Console.Write("Search for artist: ");
string searchQuery = Console.ReadLine();
var artists = await FindArtistAsync(searchQuery);
foreach (var artist in artists)
Console.WriteLine(artist);
}
static async Task<IEnumerable<Artist>> FindArtistAsync(string searchQuery)
{
const string connStr = @"Server=.\SQLEXPRESS;
Initial Catalog=Music; Integrated Security=True";
using (var conn = new SqlConnection(connStr))
{
await conn.OpenAsync();
const string sql = @"select Id, Name
from Artist
where Name like @searchQuery;";
var param = new { searchQuery = $"%{searchQuery}%" };
var artists = await conn.QueryAsync<Artist>(sql, param);
return artists;
}
}
An example run of this could be as follows:
Search for artist: re
2: Red Hot Chili Peppers
3: The Cure
Another one shows that, as expected, SQL injection doesn’t work with this code (i.e. no results were returned):
Search for artist: ' OR 1=1 --
Transactions
ADO .NET transactions are already based on ADO .NET connection objects, so there is no new extension method to start, commit and rollback transactions with Dapper. However, queries involved in a transaction need to specify the transaction. For this reason, Dapper methods such as QueryAsync()
, ExecuteScalarAsync()
and ExecuteAsync()
take a parameter of type IDbTransaction
where you can pass in the relevant transacton object.
Conclusion
As you can see, Dapper takes away the tedious parts of ADO .NET and provides methods that allow you to run queries with just a connection obect. There’s no need to set up command objects, spend a lot of time preparing parameters, or map and convert returned fields one by one.
We haven’t covered everything you can do with Dapper, so feel free to check out the links at the beginning of this article to learn more.
This is also the end of this mini-series on ADO .NET. Hopefully, by now you can see that it’s actually quite easy to run queries with this good old technology, and it doesn’t have to be tedious. You can still use ORMs like Entity Framework if you like, but you should be able to weigh the pros and cons of your choice.