DEV Community

Cover image for Simplifying Dynamic SQL Queries with Dapper
Mishael Ogochukwu
Mishael Ogochukwu

Posted on • Edited on

Simplifying Dynamic SQL Queries with Dapper

Introduction

Dapper is an open-source, lightweight Object-Relational Mapping (ORM) library for .NET. Dapper simplifies building and executing SQL queries, offering a rich set of tools for seamless integration with databases.

In this article, we will delve into practical examples of using Dapper to interact with an SQLite database, highlighting its strengths. Additionally, we will introduce a valuable companion library, Dapper.SimpleSqlBuilder, designed to enhance the Dapper experience by streamlining the process of constructing dynamic SQL queries.

Getting Started with Dapper

Let us begin by examining a common scenario. Suppose you have a table named Users in an SQLite database, and you want to retrieve users with a UserTypeId of 4 and a Role of Admin. The following Dapper code accomplishes this:

using Dapper;
using Microsoft.Data.Sqlite;

var userTypeId = 4;
var role = "Admin";

var sql = @"
SELECT * FROM Users
WHERE UserTypeId = @userTypeId
AND Role = @role";

using var connection = new SqliteConnection("Data Source=database.db");
var users = connection.Query<User>(sql, new { userTypeId, role });
Enter fullscreen mode Exit fullscreen mode

This concise code demonstrates the power and simplicity of Dapper for executing SQL queries and retrieving data from a database.

Introducing Dapper.SimpleSqlBuilder

What is Dapper.SimpleSqlBuilder?

Dapper.SimpleSqlBuilder is a library that enhances the Dapper experience by providing a simple, efficient, and fluent way to build both static and dynamic SQL queries. Leveraging string interpolation and fluent API, this library allows developers to construct safe and parameterized SQL queries with ease.

This is not a novel idea, as there have been articles written about this, and there are other libraries that do similar things. Still, I wanted to build something that was simple, easy to use, memory efficient, fast, safe and created parametrized SQL queries.

Key Features

  • Provides a simple and natural way to write SQL queries using string interpolation.
  • Chainable methods and fluent APIs for building SQL queries.
  • Supports parameter reuse in queries.
  • Dependency injection support.
  • Conditional methods for building dynamic SQL queries.
  • Performant and memory efficient. Performs similarly or better when compared to Dapper's SqlBuilder.

The library provides two builders for building SQL queries:

  • Builder — for building static, dynamic and complex SQL queries.
  • Fluent Builder — for building dynamic SQL queries using fluent API.

The Builder

Let us revisit the earlier scenario but this time using the Dapper.SimpleSqlBuilder's Builder.

using Dapper;
using Dapper.SimpleSqlBuilder;
using Microsoft.Data.Sqlite;

var userTypeId = 4;
var role = "Admin";

var builder = SimpleBuilder.Create($@"
SELECT * FROM Users
WHERE UserTypeId = {userTypeId}
AND Role = {role}");

using var connection = new SqliteConnection("Data Source=database.db");
var users = connection.Query<User>(builder.Sql, builder.Parameters);
Enter fullscreen mode Exit fullscreen mode

I know some of you are already thinking, this is not safe, you are using string interpolation, and this can lead to SQL injection.

Your concerns are valid; however, the library mitigates this by capturing the values passed into the interpolated string and creating the parametrized SQL statement as seen below.

All values passed into the interpolated string are put into Dapper's DynamicParameters collection.

SELECT * FROM Users
WHERE UserTypeId = @p0
AND Role = @p1
Enter fullscreen mode Exit fullscreen mode

Using the same scenario let us make the query dynamic.

using Dapper;
using Dapper.SimpleSqlBuilder;
using Microsoft.Data.Sqlite;

var users = GetUsers(null, "Admin");

IEnumerable<User> GetUsers(int? userTypeId = null, string role = null)
{

    var builder = SimpleBuilder.Create($"SELECT * FROM Users")
      .AppendNewLine(userTypeId.HasValue || !string.IsNullOrWhiteSpace(role), $"WHERE 1 = 1")
      .Append(userTypeId.HasValue, $"AND UserTypeId = {userTypeId}")
      .Append(!string.IsNullOrWhiteSpace(role), $"AND Role = {role}");

    using var connection = new SqliteConnection("Data Source=database.db");
    var users = connection.Query<User>(builder.Sql, builder.Parameters);
}
Enter fullscreen mode Exit fullscreen mode

The generated SQL will be:

SELECT * FROM Users
WHERE 1 = 1 AND Role = @p0
Enter fullscreen mode Exit fullscreen mode

You can check out the complete feature set of the Builder in the documentation.

Let's quickly talk about the SQL injection

We are all aware of the dangers of SQL injection, however, if you are not, I suggest you read up on it here.

So how does the library prevent this?

The library mitigates this by forcing you to write all your SQL queries using string interpolation, this is to ensure that values passed into the interpolated string are captured and parametrized. Due to this constraint, the code below won't compile.

// Scenario 1: Won't compile
var builder = SimpleBuilder.Create("SELECT * FROM User");

// Scenario 2: Won't compile
var sql = "SELECT * FROM User";
builder = SimpleBuilder.Create(sql);

// Scenario 3: Won't compile
builder = SimpleBuilder.Create(sql + " WHERE ROLE IS NOT NULL");

// Scenario 4: Won't compile
sql = $"SELECT * FROM User WHERE UserTypeId = {userTypeId}";
builder = SimpleBuilder.Create(sql);

// Scenario 5: Won't compile
builder = SimpleBuilder.Create(sql + $" AND Role = {role}");
Enter fullscreen mode Exit fullscreen mode

The Fluent Builder

The Fluent Builder offers a more expressive way to build dynamic SQL queries with fluent APIs and supports SELECT, INSERT, UPDATE and DELETE operations.

We will only look at the Select operation to keep this article short and sweet. However, you can learn more about the other operations and features of the Fluent Builder in the documentation.

Select Builder

Using the same scenario as mentioned earlier, we will use the Fluent Builder to build our SQL query.

var userTypeId = 4;
var role = "Admin";

var builder = SimpleBuilder.CreateFluent()
    .Select($"*")
    .From($"Users")
    .Where($"UserTypeId = {userTypeId}")
    .Where($"Role = {role}");

using var connection = new SqliteConnection("Data Source=database.db");
var users = connection.Query<User>(builder.Sql, builder.Parameters);
Enter fullscreen mode Exit fullscreen mode

The generated SQL will be:

SELECT *
FROM Users
WHERE UserTypeId = @p0 AND Role = @p1
Enter fullscreen mode Exit fullscreen mode

Let us look at another example but this time we will make the SQL query dynamic.

var filter = new Filter { UserTypeId = null, Roles = new [] { "Admin", "User" }, IncludeUsersWithoutRole = true };
var users = GetUsers(filter);

IEnumerable<User> GetUsers(Filter? filter = null)
{
    var builder = SimpleBuilder.CreateFluent()
        .Select($"*")
        .From($"User")
        .Where(filter?.UserTypeId.HasValue == true, $"UserTypeId = {filter.UserTypeId}")
        .OrWhere(filter?.Roles?.Length > 0, $"Role IN {filter.Roles}")
        .OrWhere(filter?.IncludeUsersWithoutRole == true, $"Role IS NULL");

    using var connection = new SqliteConnection("Data Source=database.db");
    return connection.Query<User>(builder.Sql, builder.Parameters);
}
Enter fullscreen mode Exit fullscreen mode

The generated SQL will be:

SELECT *
FROM Users
WHERE Role IN @p0 OR Role IS NULL
Enter fullscreen mode Exit fullscreen mode

The Select Builder also has support for:

  • Distinct, Joins, OrderBy, Having and GroupBy clauses.
  • Pagination: Limit, Offset and Fetch clauses.
  • Where Filters (Complex filter statements).

How does it compare to Dapper?

The code below shows how the library compares to Dapper and Dapper's SqlBuilder.

using var connection = new SqliteConnection("Data Source=database.db");

// Building and executing SQL query with Dapper

var sql = @"
SELECT * FROM Users
WHERE UserTypeId = @userTypeId 
AND Role = @role";

var users = connection.Query<User>(sql, new { userTypeId, role })

// Building and executing SQL query with Dapper's SqlBuilder

var sqlBuilder = new SqlBuilder()
    .Where("UserTypeId = @userTypeId", new { userTypeId })
    .Where("Role = @role", new { role });

var template = sqlBuilder.AddTemplate("SELECT * FROM Users /**where**/");
users = connection.Query<User>(template.RawSql, template.Parameters);

// Building and executing SQL query with the Builder (Dapper.SimpleSqlBuilder)

var builder = SimpleBuilder.Create($@"
SELECT * FROM Users
WHERE UserTypeId = {userTypeId}
AND Role = {role}");

users = connection.Query<User>(builder.Sql, builder.Parameters);

// Building and executing SQL query with the Fluent Builder (Dapper.SimpleSqlBuilder)

var fluentBuilder = SimpleBuilder.CreateFluent()
    .Select($"*")
    .From($"Users")
    .Where($"UserTypeId = {userTypeId}")
    .Where($"Role = {role}");

users = connection.Query<User>(fluentBuilder.Sql, fluentBuilder.Parameters);
Enter fullscreen mode Exit fullscreen mode

As you can see, the library alleviates some of the ceremonies required when using Dapper and provides a simple and natural way of writing SQL queries.

Performance

Performance is always relative and depends on the scenario and other factors (e.g., hardware, OS, etc), however, the results below give a good indication of how the library performs.

The benchmark shows the performance of the Builder and Fluent Builder compared to Dapper's SqlBuilder for building queries only (this does not benchmark SQL execution).


BenchmarkDotNet v0.13.12, Windows 11 (10.0.26100.2454)
Intel Core i7-8750H CPU 2.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK 9.0.100
  [Host]     : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
  Job-AYCPZN : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
  Job-FCIXJG : .NET Framework 4.8.1 (4.8.9290.0), X64 RyuJIT VectorSize=256

Enter fullscreen mode Exit fullscreen mode
Method Runtime Categories Mean Allocated
SqlBuilder (Dapper) .NET 9.0 Simple query 1.641 μs 2.92 KB
Builder .NET 9.0 Simple query 1.223 μs 4.42 KB
FluentBuilder .NET 9.0 Simple query 1.435 μs 4.5 KB
Builder (Reuse parameters) .NET 9.0 Simple query 1.900 μs 4.7 KB
FluentBuilder (Reuse parameters) .NET 9.0 Simple query 2.108 μs 4.77 KB
SqlBuilder (Dapper) .NET Framework 4.6.2 Simple query 3.448 μs 3.43 KB
Builder .NET Framework 4.6.2 Simple query 4.031 μs 4.69 KB
FluentBuilder .NET Framework 4.6.2 Simple query 4.698 μs 5.2 KB
Builder (Reuse parameters) .NET Framework 4.6.2 Simple query 4.985 μs 5.27 KB
FluentBuilder (Reuse parameters) .NET Framework 4.6.2 Simple query 5.805 μs 5.77 KB
SqlBuilder (Dapper) .NET 9.0 Large query 27.723 μs 42.19 KB
Builder .NET 9.0 Large query 16.502 μs 48.78 KB
FluentBuilder .NET 9.0 Large query 19.042 μs 48.62 KB
Builder (Reuse parameters) .NET 9.0 Large query 13.741 μs 29.34 KB
FluentBuilder (Reuse parameters) .NET 9.0 Large query 15.446 μs 29.18 KB
SqlBuilder (Dapper) .NET Framework 4.6.2 Large query 47.106 μs 53.1 KB
Builder .NET Framework 4.6.2 Large query 56.699 μs 62.15 KB
FluentBuilder .NET Framework 4.6.2 Large query 68.462 μs 68.61 KB
Builder (Reuse parameters) .NET Framework 4.6.2 Large query 43.441 μs 37.42 KB
FluentBuilder (Reuse parameters) .NET Framework 4.6.2 Large query 53.723 μs 43.87 KB

The benchmark results are valid at the time of writing. To view the latest benchmark results, refer to the benchmark page for more details.

Conclusion

I hope you enjoyed reading this article and learned something new. Dapper.SimpleSqlBuilder is a cool library (I would hope it is 😄) that meets a specific need and I hope you find it useful.

If you like the library, use it, share it, and give it a ⭐️ on GitHub. For any questions, comments, or feedback, please feel free to reach out to me on GitHub.

Top comments (0)