Latest commit

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Dapper Query Builder

Dapper Query Builder using String Interpolation and Fluent API

We all love Dapper and how Dapper is a minimalist library.

This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. It's based on 2 fundamentals:

Fundamental 1: String Interpolation instead of manually using DynamicParameters

By using interpolated strings we can pass parameters to Dapper without having to worry about creating and managing DynamicParameters manually.
You can build your queries with interpolated strings, and this library will automatically "parametrize" your values.

(If you just passed an interpolated string to Dapper, you would have to manually sanitize your inputs against SQL-injection attacks, and on top of that your queries wouldn't benefit from cached execution plan).

Instead of writing like this:

varproducts=cn.Query<Product>($@" SELECT * FROM [Product] WHERE [Name] LIKE @productName AND [ProductSubcategoryID] = @subCategoryId ORDER BY [ProductId]",new{productName,subCategoryId});

... you can just write like this:

varproducts=cn.QueryBuilder($@" SELECT * FROM [Product] WHERE [Name] LIKE {productName} AND [ProductSubcategoryID] = {subCategoryId} ORDER BY [ProductId]").Query<Product>;

The underlying query will be fully parametrized ([Name] LIKE @p0 AND [ProductSubcategoryID] = @p1), without risk of SQL-injection, even though it looks like you're just building dynamic sql.

Fundamental 2: Query and Parameters walk side-by-side

QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with your query.
This is a simple concept but it allows us to add new sql clauses (parametrized) in a single statement.

Let's say you're building a query with a variable number of conditions. Instead of appending multiple conditions like this:

vardynamicParams=newDynamicParameters();stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=" AND Name LIKE @productName";dynamicParams.Add("productName",productName);sql+=" AND ProductSubcategoryID = @subCategoryId";dynamicParams.Add("subCategoryId",subCategoryId);varproducts=cn.Query<Product>(sql,dynamicParams);// or like this:stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=$" AND Name LIKE {productName.Replace("'","''")}";sql+=$" AND ProductSubcategoryID = {subCategoryId.Replace("'","''")}";// here is where you pray that you've correctly sanitized inputs against sql-injectionvarproducts=cn.Query<Product>(sql);

... you can just write like this:

varquery=cn.QueryBuilder($"SELECT * FROM [Product] WHERE 1=1");query.Append($"AND Name LIKE {productName}");query.Append($"AND ProductSubcategoryID = {subCategoryId}");varproducts=query.Query<Product>();

QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily.
When you invoke Query, the underlying query and parameters are passed to Dapper.

Quickstart / NuGet Package

  1. Install the NuGet package Dapper-QueryBuilder
  2. Start using like this:
usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varproducts=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [ListPrice] <= {maxPrice} AND [Weight] <= {maxWeight} AND [Name] LIKE {search} ORDER BY ProductId").Query<Product>();

Or building dynamic conditions like this:

usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varq=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1 ");q.AppendLine("AND [ListPrice] <= {maxPrice}");q.AppendLine("AND [Weight] <= {maxWeight}");q.AppendLine("AND [Name] LIKE {search}");q.AppendLine("ORDER BY ProductId");varproducts=q.Query<Product>();

Full Documentation and Extra features

Filters as First-class citizen

As shown above, you'll still write plain SQL, which is what we all love about Dapper.
Since the most common use case for dynamic clauses is adding WHERE parameters, the library offers WHERE filters as a special structure:

  • You can add filters to QueryBuilder using .Where() method, those filters are saved internally
  • When you send your query to Dapper, QueryBuilder will search for a /**where**/ statement in your query and will replace with the filters you defined.

So you can still write your queries on your own, and yet benefit from string interpolation (which is our mojo and charm) and from dynamically building a list of filters.

intmaxPrice=1000;intmaxWeight=15;stringsearch="%Mountain%";varcn=newSqlConnection(connectionString);// You can build the query manually and just use QueryBuilder to replace "where" filters (if any)varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");// You just pass the parameters as if it was an interpolated string, // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe)q.Where($"[ListPrice] <= {maxPrice}");q.Where($"[Weight] <= {maxWeight}");q.Where($"[Name] LIKE {search}");// Query() will automatically build your query and replace your /**where**/ (if any filter was added)varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

If you don't need the WHERE keyword (if you already have other fixed conditions before), you can use /**filters**/ instead:

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [Price]>{minPrice} /**filters**/ ORDER BY ProductId ");

Combining Filters

QueryBuilder contains an internal property called "Filters" which just keeps track of all conditions you've added using .Where() method.
All those conditions by default are combined with AND operator.

If you want to write more complex filters (combining multiple AND/OR filters) we have a typed structure for that, like other query builders do. But differently from other builders, we don't try to reinvent SQL syntax or create a limited abstraction over SQL language, which is powerful, comprehensive, and vendor-specific, so you should still write your raw filters as if they were regular strings, and we do the rest (structuring AND/OR filters, and extracting parameters from interpolated strings):

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");q.Where(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});q.Where(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});varproducts=q.Query<Product>();// Query() will automatically build your SQL query, and will replace your /**where**/ (if any filter was added)// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// it will also pass an underlying DynamicParameters object, with all parameters you passed using string interpolation // (@p0 as minPrice, @p1 as maxPrice, etc..)

Raw command building

If you don't like the "magic" of replacing /**where**/ filters, you can do everything on your own.

// start your basic queryvarq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1");// append whatever statements you need (.Append instead of .Where!)q.Append($"AND [ListPrice] <= {maxPrice}");q.Append($"AND [Weight] <= {maxWeight}");q.Append($"AND [Name] LIKE {search}");q.Append($"ORDER BY ProductId");varproducts=q.Query<Product>();

varchar vs nvarchar

Dapper has an issue with strings because they are assumed to be unicode strings (nvarchar) by default.
That works, but does not give the best performance - in some cases you may prefer to explicitly describe if your strings are unicode or ansi (nvarchar or varchar), and also describe their exact sizes.

Instead of using Dapper DbString class, you can just pass explicit type for your parameters, like this:

// start your basic querystringproductName="Mountain%";varquery=cn.QueryBuilder($@" SELECT * FROM [Production].[Product] p  WHERE [Name] LIKE {productName:nvarchar(20)}");

You can use sql types like varchar(size), nvarchar(size), char(size), nchar(size), varchar(MAX), nvarchar(MAX). (If your database does not use this exact types, Dapper will convert them to your database. We pass DbStrings to Dapper and use the hints above to define if they IsAnsi and IsFixedLength.

nvarchar and nchar are unicode strings, while varchar and char are ansi strings.
nvarchar and varchar are variable-length strings, while nchar and char are fixed-length strings.

IN lists

Dapper allows us to use IN lists magically. And it also works with our string interpolation:

varq=cn.QueryBuilder($@" SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] FROM [Product] p INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");varcategories=newstring[]{"Components","Clothing","Acessories"};q.Append($"WHERE c.[Name] IN {categories}");

Fluent API (Chained-methods)

For those who like method-chaining guidance (or for those who allow end-users to build their own queries), there's a Fluent API which allows you to build queries step-by-step mimicking dynamic SQL concatenation.

So, basically, instead of starting with a full query and just appending new filters (.Where()), the QueryBuilder will build the whole query for you:

varq=cn.QueryBuilder().Select($"ProductId").Select($"Name").Select($"ListPrice").Select($"Weight").From($"[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy($"ProductId");varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

Or more elaborated:

varq=cn.QueryBuilder().SelectDistinct($"ProductId, Name, ListPrice, Weight").From("[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy("ProductId");

Building joins dynamically using Fluent API:

varcategories=newstring[]{"Components","Clothing","Acessories"};varq=cn.QueryBuilder().SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]").From($"[Product] p").From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]").From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]").Where($"c.[Name] IN {categories}");

There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY).

nameof() and raw strings

For those who like strongly typed queries, you can also use nameof expression, but you have to define format "raw" such that the string is preserved and it's not converted into a @parameter.

varq=cn.QueryBuilder($@" SELECT c.[{nameof(Category.Name):raw}] as [Category],  sc.[{nameof(Subcategory.Name):raw}] as [Subcategory],  p.[{nameof(Product.Name):raw}], p.[ProductNumber]"
FROM [Product] p
INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]INNER JOIN [ProductCategory]c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");

And in case you can use "find references", "rename" (refactor), etc.

Using Type-Safe Filters without QueryBuilder

If for any reason you don't want to use our QueryBuilder, you can still use type-safe dynamic filters:

Dapper.DynamicParametersparms=newDapper.DynamicParameters();varfilters=newFilters(Filters.FiltersType.AND);filters.Add(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});filters.Add(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});stringwhere=filters.BuildFilters(parms);// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// parms contains @p0 as minPrice, @p1 as maxPrice, etc..

Invoking Stored Procedures

// This is basically Dapper, but with a FluentAPI where you can append parameters dynamically.varq=cn.CommandBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]").AddParameter("ReturnValue",dbType:DbType.Int32,direction:ParameterDirection.ReturnValue).AddParameter("ErrorLogID",dbType:DbType.Int32,direction:ParameterDirection.Output).AddParameter("BusinessEntityID",businessEntityID).AddParameter("NationalIDNumber",nationalIDNumber).AddParameter("BirthDate",birthDate).AddParameter("MaritalStatus",maritalStatus).AddParameter("Gender",gender);intaffected=q.Execute(commandType:CommandType.StoredProcedure);intreturnValue=q.Parameters.Get<int>("ReturnValue");

How was life before this library? :-)

Building dynamic filters in Dapper was a little cumbersome / ugly:

varparms=newDynamicParameters();List<string>filters=newList<string>();filters.Add("[Name] LIKE @productName");parms.Add("productName",productName);filters.Add("[CategoryId] = @categoryId");parms.Add("categoryId",categoryId);stringwhere=(filters.Any()?" WHERE "+string.Join(" AND ",filters):"");varproducts=cn.Query<Product>($@" SELECT * FROM [Product]"{where}
ORDER BY[ProductId]",parms);

Now with DapperQueryBuilder it's much easier to write queries with dynamic filters:

varquery=cn.QueryBuilder(@" SELECT * FROM [Product]  /**where**/  ORDER BY [ProductId]").Where($"[Name] LIKE {productName}").Where($"[CategoryId] = {categoryId}");varproducts=query.Query<Product>();

Database Compatibility

QueryBuilder is database agnostic - it should work with any database, because it basically only helps to pass parameters - it does not generate SQL statements (except simple clauses like WHERE, AND, if you're using /**where**/). It was tested with Microsoft SQL Server and with PostgreSQL (using Npgsql driver), and works fine in both.

If your database driver does not accept "at-parameters" (@p0, @p1, etc), then you can just modify InterpolatedStatementParser.AutoGeneratedParameterPrefix:

// Default value is "@p", some database vendors may not accept "@" parametersInterpolatedStatementParser.AutoGeneratedParameterPrefix=":p";stringsearch="%Dinosaur%";varcmd=cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}");// Underlying SQL will be: SELECT * FROM film WHERE title like :p0

PS: Npgsql accepts "at-parameters" (@p0, @p1, etc) and will convert/pass them correctly to PostgreSQL - so you don't need to use this for Npgsql.

Collaborate

This is a brand new project, and your contribution can help a lot.

Would you like to collaborate?

Please submit a pull-request or if you prefer you can create an issue or contact me to discuss your idea.

License

MIT License

About

Dapper Query Builder using String Interpolation and Fluent API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Dapper Query Builder

Dapper Query Builder using String Interpolation and Fluent API

We all love Dapper and how Dapper is a minimalist library.

This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. It's based on 2 fundamentals:

Fundamental 1: String Interpolation instead of manually using DynamicParameters

By using interpolated strings we can pass parameters to Dapper without having to worry about creating and managing DynamicParameters manually.
You can build your queries with interpolated strings, and this library will automatically "parametrize" your values.

(If you just passed an interpolated string to Dapper, you would have to manually sanitize your inputs against SQL-injection attacks, and on top of that your queries wouldn't benefit from cached execution plan).

Instead of writing like this:

varproducts=cn.Query<Product>($@" SELECT * FROM [Product] WHERE [Name] LIKE @productName AND [ProductSubcategoryID] = @subCategoryId ORDER BY [ProductId]",new{productName,subCategoryId});

... you can just write like this:

varproducts=cn.QueryBuilder($@" SELECT * FROM [Product] WHERE [Name] LIKE {productName} AND [ProductSubcategoryID] = {subCategoryId} ORDER BY [ProductId]").Query<Product>;

The underlying query will be fully parametrized ([Name] LIKE @p0 AND [ProductSubcategoryID] = @p1), without risk of SQL-injection, even though it looks like you're just building dynamic sql.

Fundamental 2: Query and Parameters walk side-by-side

QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with your query.
This is a simple concept but it allows us to add new sql clauses (parametrized) in a single statement.

Let's say you're building a query with a variable number of conditions. Instead of appending multiple conditions like this:

vardynamicParams=newDynamicParameters();stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=" AND Name LIKE @productName";dynamicParams.Add("productName",productName);sql+=" AND ProductSubcategoryID = @subCategoryId";dynamicParams.Add("subCategoryId",subCategoryId);varproducts=cn.Query<Product>(sql,dynamicParams);// or like this:stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=$" AND Name LIKE {productName.Replace("'","''")}";sql+=$" AND ProductSubcategoryID = {subCategoryId.Replace("'","''")}";// here is where you pray that you've correctly sanitized inputs against sql-injectionvarproducts=cn.Query<Product>(sql);

... you can just write like this:

varquery=cn.QueryBuilder($"SELECT * FROM [Product] WHERE 1=1");query.Append($"AND Name LIKE {productName}");query.Append($"AND ProductSubcategoryID = {subCategoryId}");varproducts=query.Query<Product>();

QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily.
When you invoke Query, the underlying query and parameters are passed to Dapper.

Quickstart / NuGet Package

  1. Install the NuGet package Dapper-QueryBuilder
  2. Start using like this:
usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varproducts=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [ListPrice] <= {maxPrice} AND [Weight] <= {maxWeight} AND [Name] LIKE {search} ORDER BY ProductId").Query<Product>();

Or building dynamic conditions like this:

usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varq=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1 ");q.AppendLine("AND [ListPrice] <= {maxPrice}");q.AppendLine("AND [Weight] <= {maxWeight}");q.AppendLine("AND [Name] LIKE {search}");q.AppendLine("ORDER BY ProductId");varproducts=q.Query<Product>();

Full Documentation and Extra features

Filters as First-class citizen

As shown above, you'll still write plain SQL, which is what we all love about Dapper.
Since the most common use case for dynamic clauses is adding WHERE parameters, the library offers WHERE filters as a special structure:

  • You can add filters to QueryBuilder using .Where() method, those filters are saved internally
  • When you send your query to Dapper, QueryBuilder will search for a /**where**/ statement in your query and will replace with the filters you defined.

So you can still write your queries on your own, and yet benefit from string interpolation (which is our mojo and charm) and from dynamically building a list of filters.

intmaxPrice=1000;intmaxWeight=15;stringsearch="%Mountain%";varcn=newSqlConnection(connectionString);// You can build the query manually and just use QueryBuilder to replace "where" filters (if any)varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");// You just pass the parameters as if it was an interpolated string, // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe)q.Where($"[ListPrice] <= {maxPrice}");q.Where($"[Weight] <= {maxWeight}");q.Where($"[Name] LIKE {search}");// Query() will automatically build your query and replace your /**where**/ (if any filter was added)varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

If you don't need the WHERE keyword (if you already have other fixed conditions before), you can use /**filters**/ instead:

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [Price]>{minPrice} /**filters**/ ORDER BY ProductId ");

Combining Filters

QueryBuilder contains an internal property called "Filters" which just keeps track of all conditions you've added using .Where() method.
All those conditions by default are combined with AND operator.

If you want to write more complex filters (combining multiple AND/OR filters) we have a typed structure for that, like other query builders do. But differently from other builders, we don't try to reinvent SQL syntax or create a limited abstraction over SQL language, which is powerful, comprehensive, and vendor-specific, so you should still write your raw filters as if they were regular strings, and we do the rest (structuring AND/OR filters, and extracting parameters from interpolated strings):

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");q.Where(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});q.Where(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});varproducts=q.Query<Product>();// Query() will automatically build your SQL query, and will replace your /**where**/ (if any filter was added)// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// it will also pass an underlying DynamicParameters object, with all parameters you passed using string interpolation // (@p0 as minPrice, @p1 as maxPrice, etc..)

Raw command building

If you don't like the "magic" of replacing /**where**/ filters, you can do everything on your own.

// start your basic queryvarq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1");// append whatever statements you need (.Append instead of .Where!)q.Append($"AND [ListPrice] <= {maxPrice}");q.Append($"AND [Weight] <= {maxWeight}");q.Append($"AND [Name] LIKE {search}");q.Append($"ORDER BY ProductId");varproducts=q.Query<Product>();

varchar vs nvarchar

Dapper has an issue with strings because they are assumed to be unicode strings (nvarchar) by default.
That works, but does not give the best performance - in some cases you may prefer to explicitly describe if your strings are unicode or ansi (nvarchar or varchar), and also describe their exact sizes.

Instead of using Dapper DbString class, you can just pass explicit type for your parameters, like this:

// start your basic querystringproductName="Mountain%";varquery=cn.QueryBuilder($@" SELECT * FROM [Production].[Product] p  WHERE [Name] LIKE {productName:nvarchar(20)}");

You can use sql types like varchar(size), nvarchar(size), char(size), nchar(size), varchar(MAX), nvarchar(MAX). (If your database does not use this exact types, Dapper will convert them to your database. We pass DbStrings to Dapper and use the hints above to define if they IsAnsi and IsFixedLength.

nvarchar and nchar are unicode strings, while varchar and char are ansi strings.
nvarchar and varchar are variable-length strings, while nchar and char are fixed-length strings.

IN lists

Dapper allows us to use IN lists magically. And it also works with our string interpolation:

varq=cn.QueryBuilder($@" SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] FROM [Product] p INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");varcategories=newstring[]{"Components","Clothing","Acessories"};q.Append($"WHERE c.[Name] IN {categories}");

Fluent API (Chained-methods)

For those who like method-chaining guidance (or for those who allow end-users to build their own queries), there's a Fluent API which allows you to build queries step-by-step mimicking dynamic SQL concatenation.

So, basically, instead of starting with a full query and just appending new filters (.Where()), the QueryBuilder will build the whole query for you:

varq=cn.QueryBuilder().Select($"ProductId").Select($"Name").Select($"ListPrice").Select($"Weight").From($"[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy($"ProductId");varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

Or more elaborated:

varq=cn.QueryBuilder().SelectDistinct($"ProductId, Name, ListPrice, Weight").From("[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy("ProductId");

Building joins dynamically using Fluent API:

varcategories=newstring[]{"Components","Clothing","Acessories"};varq=cn.QueryBuilder().SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]").From($"[Product] p").From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]").From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]").Where($"c.[Name] IN {categories}");

There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY).

nameof() and raw strings

For those who like strongly typed queries, you can also use nameof expression, but you have to define format "raw" such that the string is preserved and it's not converted into a @parameter.

varq=cn.QueryBuilder($@" SELECT c.[{nameof(Category.Name):raw}] as [Category],  sc.[{nameof(Subcategory.Name):raw}] as [Subcategory],  p.[{nameof(Product.Name):raw}], p.[ProductNumber]"
FROM [Product] p
INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]INNER JOIN [ProductCategory]c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");

And in case you can use "find references", "rename" (refactor), etc.

Using Type-Safe Filters without QueryBuilder

If for any reason you don't want to use our QueryBuilder, you can still use type-safe dynamic filters:

Dapper.DynamicParametersparms=newDapper.DynamicParameters();varfilters=newFilters(Filters.FiltersType.AND);filters.Add(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});filters.Add(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});stringwhere=filters.BuildFilters(parms);// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// parms contains @p0 as minPrice, @p1 as maxPrice, etc..

Invoking Stored Procedures

// This is basically Dapper, but with a FluentAPI where you can append parameters dynamically.varq=cn.CommandBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]").AddParameter("ReturnValue",dbType:DbType.Int32,direction:ParameterDirection.ReturnValue).AddParameter("ErrorLogID",dbType:DbType.Int32,direction:ParameterDirection.Output).AddParameter("BusinessEntityID",businessEntityID).AddParameter("NationalIDNumber",nationalIDNumber).AddParameter("BirthDate",birthDate).AddParameter("MaritalStatus",maritalStatus).AddParameter("Gender",gender);intaffected=q.Execute(commandType:CommandType.StoredProcedure);intreturnValue=q.Parameters.Get<int>("ReturnValue");

How was life before this library? :-)

Building dynamic filters in Dapper was a little cumbersome / ugly:

varparms=newDynamicParameters();List<string>filters=newList<string>();filters.Add("[Name] LIKE @productName");parms.Add("productName",productName);filters.Add("[CategoryId] = @categoryId");parms.Add("categoryId",categoryId);stringwhere=(filters.Any()?" WHERE "+string.Join(" AND ",filters):"");varproducts=cn.Query<Product>($@" SELECT * FROM [Product]"{where}
ORDER BY[ProductId]",parms);

Now with DapperQueryBuilder it's much easier to write queries with dynamic filters:

varquery=cn.QueryBuilder(@" SELECT * FROM [Product]  /**where**/  ORDER BY [ProductId]").Where($"[Name] LIKE {productName}").Where($"[CategoryId] = {categoryId}");varproducts=query.Query<Product>();

Database Compatibility

QueryBuilder is database agnostic - it should work with any database, because it basically only helps to pass parameters - it does not generate SQL statements (except simple clauses like WHERE, AND, if you're using /**where**/). It was tested with Microsoft SQL Server and with PostgreSQL (using Npgsql driver), and works fine in both.

If your database driver does not accept "at-parameters" (@p0, @p1, etc), then you can just modify InterpolatedStatementParser.AutoGeneratedParameterPrefix:

// Default value is "@p", some database vendors may not accept "@" parametersInterpolatedStatementParser.AutoGeneratedParameterPrefix=":p";stringsearch="%Dinosaur%";varcmd=cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}");// Underlying SQL will be: SELECT * FROM film WHERE title like :p0

PS: Npgsql accepts "at-parameters" (@p0, @p1, etc) and will convert/pass them correctly to PostgreSQL - so you don't need to use this for Npgsql.

Collaborate

This is a brand new project, and your contribution can help a lot.

Would you like to collaborate?

Please submit a pull-request or if you prefer you can create an issue or contact me to discuss your idea.

License

MIT License

About

Dapper Query Builder using String Interpolation and Fluent API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Dapper Query Builder

Dapper Query Builder using String Interpolation and Fluent API

We all love Dapper and how Dapper is a minimalist library.

This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. It's based on 2 fundamentals:

Fundamental 1: String Interpolation instead of manually using DynamicParameters

By using interpolated strings we can pass parameters to Dapper without having to worry about creating and managing DynamicParameters manually.
You can build your queries with interpolated strings, and this library will automatically "parametrize" your values.

(If you just passed an interpolated string to Dapper, you would have to manually sanitize your inputs against SQL-injection attacks, and on top of that your queries wouldn't benefit from cached execution plan).

Instead of writing like this:

varproducts=cn.Query<Product>($@" SELECT * FROM [Product] WHERE [Name] LIKE @productName AND [ProductSubcategoryID] = @subCategoryId ORDER BY [ProductId]",new{productName,subCategoryId});

... you can just write like this:

varproducts=cn.QueryBuilder($@" SELECT * FROM [Product] WHERE [Name] LIKE {productName} AND [ProductSubcategoryID] = {subCategoryId} ORDER BY [ProductId]").Query<Product>;

The underlying query will be fully parametrized ([Name] LIKE @p0 AND [ProductSubcategoryID] = @p1), without risk of SQL-injection, even though it looks like you're just building dynamic sql.

Fundamental 2: Query and Parameters walk side-by-side

QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with your query.
This is a simple concept but it allows us to add new sql clauses (parametrized) in a single statement.

Let's say you're building a query with a variable number of conditions. Instead of appending multiple conditions like this:

vardynamicParams=newDynamicParameters();stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=" AND Name LIKE @productName";dynamicParams.Add("productName",productName);sql+=" AND ProductSubcategoryID = @subCategoryId";dynamicParams.Add("subCategoryId",subCategoryId);varproducts=cn.Query<Product>(sql,dynamicParams);// or like this:stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=$" AND Name LIKE {productName.Replace("'","''")}";sql+=$" AND ProductSubcategoryID = {subCategoryId.Replace("'","''")}";// here is where you pray that you've correctly sanitized inputs against sql-injectionvarproducts=cn.Query<Product>(sql);

... you can just write like this:

varquery=cn.QueryBuilder($"SELECT * FROM [Product] WHERE 1=1");query.Append($"AND Name LIKE {productName}");query.Append($"AND ProductSubcategoryID = {subCategoryId}");varproducts=query.Query<Product>();

QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily.
When you invoke Query, the underlying query and parameters are passed to Dapper.

Quickstart / NuGet Package

  1. Install the NuGet package Dapper-QueryBuilder
  2. Start using like this:
usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varproducts=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [ListPrice] <= {maxPrice} AND [Weight] <= {maxWeight} AND [Name] LIKE {search} ORDER BY ProductId").Query<Product>();

Or building dynamic conditions like this:

usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varq=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1 ");q.AppendLine("AND [ListPrice] <= {maxPrice}");q.AppendLine("AND [Weight] <= {maxWeight}");q.AppendLine("AND [Name] LIKE {search}");q.AppendLine("ORDER BY ProductId");varproducts=q.Query<Product>();

Full Documentation and Extra features

Filters as First-class citizen

As shown above, you'll still write plain SQL, which is what we all love about Dapper.
Since the most common use case for dynamic clauses is adding WHERE parameters, the library offers WHERE filters as a special structure:

  • You can add filters to QueryBuilder using .Where() method, those filters are saved internally
  • When you send your query to Dapper, QueryBuilder will search for a /**where**/ statement in your query and will replace with the filters you defined.

So you can still write your queries on your own, and yet benefit from string interpolation (which is our mojo and charm) and from dynamically building a list of filters.

intmaxPrice=1000;intmaxWeight=15;stringsearch="%Mountain%";varcn=newSqlConnection(connectionString);// You can build the query manually and just use QueryBuilder to replace "where" filters (if any)varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");// You just pass the parameters as if it was an interpolated string, // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe)q.Where($"[ListPrice] <= {maxPrice}");q.Where($"[Weight] <= {maxWeight}");q.Where($"[Name] LIKE {search}");// Query() will automatically build your query and replace your /**where**/ (if any filter was added)varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

If you don't need the WHERE keyword (if you already have other fixed conditions before), you can use /**filters**/ instead:

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [Price]>{minPrice} /**filters**/ ORDER BY ProductId ");

Combining Filters

QueryBuilder contains an internal property called "Filters" which just keeps track of all conditions you've added using .Where() method.
All those conditions by default are combined with AND operator.

If you want to write more complex filters (combining multiple AND/OR filters) we have a typed structure for that, like other query builders do. But differently from other builders, we don't try to reinvent SQL syntax or create a limited abstraction over SQL language, which is powerful, comprehensive, and vendor-specific, so you should still write your raw filters as if they were regular strings, and we do the rest (structuring AND/OR filters, and extracting parameters from interpolated strings):

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");q.Where(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});q.Where(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});varproducts=q.Query<Product>();// Query() will automatically build your SQL query, and will replace your /**where**/ (if any filter was added)// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// it will also pass an underlying DynamicParameters object, with all parameters you passed using string interpolation // (@p0 as minPrice, @p1 as maxPrice, etc..)

Raw command building

If you don't like the "magic" of replacing /**where**/ filters, you can do everything on your own.

// start your basic queryvarq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1");// append whatever statements you need (.Append instead of .Where!)q.Append($"AND [ListPrice] <= {maxPrice}");q.Append($"AND [Weight] <= {maxWeight}");q.Append($"AND [Name] LIKE {search}");q.Append($"ORDER BY ProductId");varproducts=q.Query<Product>();

varchar vs nvarchar

Dapper has an issue with strings because they are assumed to be unicode strings (nvarchar) by default.
That works, but does not give the best performance - in some cases you may prefer to explicitly describe if your strings are unicode or ansi (nvarchar or varchar), and also describe their exact sizes.

Instead of using Dapper DbString class, you can just pass explicit type for your parameters, like this:

// start your basic querystringproductName="Mountain%";varquery=cn.QueryBuilder($@" SELECT * FROM [Production].[Product] p  WHERE [Name] LIKE {productName:nvarchar(20)}");

You can use sql types like varchar(size), nvarchar(size), char(size), nchar(size), varchar(MAX), nvarchar(MAX). (If your database does not use this exact types, Dapper will convert them to your database. We pass DbStrings to Dapper and use the hints above to define if they IsAnsi and IsFixedLength.

nvarchar and nchar are unicode strings, while varchar and char are ansi strings.
nvarchar and varchar are variable-length strings, while nchar and char are fixed-length strings.

IN lists

Dapper allows us to use IN lists magically. And it also works with our string interpolation:

varq=cn.QueryBuilder($@" SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] FROM [Product] p INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");varcategories=newstring[]{"Components","Clothing","Acessories"};q.Append($"WHERE c.[Name] IN {categories}");

Fluent API (Chained-methods)

For those who like method-chaining guidance (or for those who allow end-users to build their own queries), there's a Fluent API which allows you to build queries step-by-step mimicking dynamic SQL concatenation.

So, basically, instead of starting with a full query and just appending new filters (.Where()), the QueryBuilder will build the whole query for you:

varq=cn.QueryBuilder().Select($"ProductId").Select($"Name").Select($"ListPrice").Select($"Weight").From($"[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy($"ProductId");varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

Or more elaborated:

varq=cn.QueryBuilder().SelectDistinct($"ProductId, Name, ListPrice, Weight").From("[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy("ProductId");

Building joins dynamically using Fluent API:

varcategories=newstring[]{"Components","Clothing","Acessories"};varq=cn.QueryBuilder().SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]").From($"[Product] p").From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]").From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]").Where($"c.[Name] IN {categories}");

There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY).

nameof() and raw strings

For those who like strongly typed queries, you can also use nameof expression, but you have to define format "raw" such that the string is preserved and it's not converted into a @parameter.

varq=cn.QueryBuilder($@" SELECT c.[{nameof(Category.Name):raw}] as [Category],  sc.[{nameof(Subcategory.Name):raw}] as [Subcategory],  p.[{nameof(Product.Name):raw}], p.[ProductNumber]"
FROM [Product] p
INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]INNER JOIN [ProductCategory]c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");

And in case you can use "find references", "rename" (refactor), etc.

Using Type-Safe Filters without QueryBuilder

If for any reason you don't want to use our QueryBuilder, you can still use type-safe dynamic filters:

Dapper.DynamicParametersparms=newDapper.DynamicParameters();varfilters=newFilters(Filters.FiltersType.AND);filters.Add(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});filters.Add(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});stringwhere=filters.BuildFilters(parms);// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// parms contains @p0 as minPrice, @p1 as maxPrice, etc..

Invoking Stored Procedures

// This is basically Dapper, but with a FluentAPI where you can append parameters dynamically.varq=cn.CommandBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]").AddParameter("ReturnValue",dbType:DbType.Int32,direction:ParameterDirection.ReturnValue).AddParameter("ErrorLogID",dbType:DbType.Int32,direction:ParameterDirection.Output).AddParameter("BusinessEntityID",businessEntityID).AddParameter("NationalIDNumber",nationalIDNumber).AddParameter("BirthDate",birthDate).AddParameter("MaritalStatus",maritalStatus).AddParameter("Gender",gender);intaffected=q.Execute(commandType:CommandType.StoredProcedure);intreturnValue=q.Parameters.Get<int>("ReturnValue");

How was life before this library? :-)

Building dynamic filters in Dapper was a little cumbersome / ugly:

varparms=newDynamicParameters();List<string>filters=newList<string>();filters.Add("[Name] LIKE @productName");parms.Add("productName",productName);filters.Add("[CategoryId] = @categoryId");parms.Add("categoryId",categoryId);stringwhere=(filters.Any()?" WHERE "+string.Join(" AND ",filters):"");varproducts=cn.Query<Product>($@" SELECT * FROM [Product]"{where}
ORDER BY[ProductId]",parms);

Now with DapperQueryBuilder it's much easier to write queries with dynamic filters:

varquery=cn.QueryBuilder(@" SELECT * FROM [Product]  /**where**/  ORDER BY [ProductId]").Where($"[Name] LIKE {productName}").Where($"[CategoryId] = {categoryId}");varproducts=query.Query<Product>();

Database Compatibility

QueryBuilder is database agnostic - it should work with any database, because it basically only helps to pass parameters - it does not generate SQL statements (except simple clauses like WHERE, AND, if you're using /**where**/). It was tested with Microsoft SQL Server and with PostgreSQL (using Npgsql driver), and works fine in both.

If your database driver does not accept "at-parameters" (@p0, @p1, etc), then you can just modify InterpolatedStatementParser.AutoGeneratedParameterPrefix:

// Default value is "@p", some database vendors may not accept "@" parametersInterpolatedStatementParser.AutoGeneratedParameterPrefix=":p";stringsearch="%Dinosaur%";varcmd=cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}");// Underlying SQL will be: SELECT * FROM film WHERE title like :p0

PS: Npgsql accepts "at-parameters" (@p0, @p1, etc) and will convert/pass them correctly to PostgreSQL - so you don't need to use this for Npgsql.

Collaborate

This is a brand new project, and your contribution can help a lot.

Would you like to collaborate?

Please submit a pull-request or if you prefer you can create an issue or contact me to discuss your idea.

License

MIT License

About

Dapper Query Builder using String Interpolation and Fluent API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Dapper Query Builder

Dapper Query Builder using String Interpolation and Fluent API

We all love Dapper and how Dapper is a minimalist library.

This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. It's based on 2 fundamentals:

Fundamental 1: String Interpolation instead of manually using DynamicParameters

By using interpolated strings we can pass parameters to Dapper without having to worry about creating and managing DynamicParameters manually.
You can build your queries with interpolated strings, and this library will automatically "parametrize" your values.

(If you just passed an interpolated string to Dapper, you would have to manually sanitize your inputs against SQL-injection attacks, and on top of that your queries wouldn't benefit from cached execution plan).

Instead of writing like this:

varproducts=cn.Query<Product>($@" SELECT * FROM [Product] WHERE [Name] LIKE @productName AND [ProductSubcategoryID] = @subCategoryId ORDER BY [ProductId]",new{productName,subCategoryId});

... you can just write like this:

varproducts=cn.QueryBuilder($@" SELECT * FROM [Product] WHERE [Name] LIKE {productName} AND [ProductSubcategoryID] = {subCategoryId} ORDER BY [ProductId]").Query<Product>;

The underlying query will be fully parametrized ([Name] LIKE @p0 AND [ProductSubcategoryID] = @p1), without risk of SQL-injection, even though it looks like you're just building dynamic sql.

Fundamental 2: Query and Parameters walk side-by-side

QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with your query.
This is a simple concept but it allows us to add new sql clauses (parametrized) in a single statement.

Let's say you're building a query with a variable number of conditions. Instead of appending multiple conditions like this:

vardynamicParams=newDynamicParameters();stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=" AND Name LIKE @productName";dynamicParams.Add("productName",productName);sql+=" AND ProductSubcategoryID = @subCategoryId";dynamicParams.Add("subCategoryId",subCategoryId);varproducts=cn.Query<Product>(sql,dynamicParams);// or like this:stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=$" AND Name LIKE {productName.Replace("'","''")}";sql+=$" AND ProductSubcategoryID = {subCategoryId.Replace("'","''")}";// here is where you pray that you've correctly sanitized inputs against sql-injectionvarproducts=cn.Query<Product>(sql);

... you can just write like this:

varquery=cn.QueryBuilder($"SELECT * FROM [Product] WHERE 1=1");query.Append($"AND Name LIKE {productName}");query.Append($"AND ProductSubcategoryID = {subCategoryId}");varproducts=query.Query<Product>();

QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily.
When you invoke Query, the underlying query and parameters are passed to Dapper.

Quickstart / NuGet Package

  1. Install the NuGet package Dapper-QueryBuilder
  2. Start using like this:
usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varproducts=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [ListPrice] <= {maxPrice} AND [Weight] <= {maxWeight} AND [Name] LIKE {search} ORDER BY ProductId").Query<Product>();

Or building dynamic conditions like this:

usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varq=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1 ");q.AppendLine("AND [ListPrice] <= {maxPrice}");q.AppendLine("AND [Weight] <= {maxWeight}");q.AppendLine("AND [Name] LIKE {search}");q.AppendLine("ORDER BY ProductId");varproducts=q.Query<Product>();

Full Documentation and Extra features

Filters as First-class citizen

As shown above, you'll still write plain SQL, which is what we all love about Dapper.
Since the most common use case for dynamic clauses is adding WHERE parameters, the library offers WHERE filters as a special structure:

  • You can add filters to QueryBuilder using .Where() method, those filters are saved internally
  • When you send your query to Dapper, QueryBuilder will search for a /**where**/ statement in your query and will replace with the filters you defined.

So you can still write your queries on your own, and yet benefit from string interpolation (which is our mojo and charm) and from dynamically building a list of filters.

intmaxPrice=1000;intmaxWeight=15;stringsearch="%Mountain%";varcn=newSqlConnection(connectionString);// You can build the query manually and just use QueryBuilder to replace "where" filters (if any)varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");// You just pass the parameters as if it was an interpolated string, // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe)q.Where($"[ListPrice] <= {maxPrice}");q.Where($"[Weight] <= {maxWeight}");q.Where($"[Name] LIKE {search}");// Query() will automatically build your query and replace your /**where**/ (if any filter was added)varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

If you don't need the WHERE keyword (if you already have other fixed conditions before), you can use /**filters**/ instead:

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [Price]>{minPrice} /**filters**/ ORDER BY ProductId ");

Combining Filters

QueryBuilder contains an internal property called "Filters" which just keeps track of all conditions you've added using .Where() method.
All those conditions by default are combined with AND operator.

If you want to write more complex filters (combining multiple AND/OR filters) we have a typed structure for that, like other query builders do. But differently from other builders, we don't try to reinvent SQL syntax or create a limited abstraction over SQL language, which is powerful, comprehensive, and vendor-specific, so you should still write your raw filters as if they were regular strings, and we do the rest (structuring AND/OR filters, and extracting parameters from interpolated strings):

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");q.Where(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});q.Where(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});varproducts=q.Query<Product>();// Query() will automatically build your SQL query, and will replace your /**where**/ (if any filter was added)// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// it will also pass an underlying DynamicParameters object, with all parameters you passed using string interpolation // (@p0 as minPrice, @p1 as maxPrice, etc..)

Raw command building

If you don't like the "magic" of replacing /**where**/ filters, you can do everything on your own.

// start your basic queryvarq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1");// append whatever statements you need (.Append instead of .Where!)q.Append($"AND [ListPrice] <= {maxPrice}");q.Append($"AND [Weight] <= {maxWeight}");q.Append($"AND [Name] LIKE {search}");q.Append($"ORDER BY ProductId");varproducts=q.Query<Product>();

varchar vs nvarchar

Dapper has an issue with strings because they are assumed to be unicode strings (nvarchar) by default.
That works, but does not give the best performance - in some cases you may prefer to explicitly describe if your strings are unicode or ansi (nvarchar or varchar), and also describe their exact sizes.

Instead of using Dapper DbString class, you can just pass explicit type for your parameters, like this:

// start your basic querystringproductName="Mountain%";varquery=cn.QueryBuilder($@" SELECT * FROM [Production].[Product] p  WHERE [Name] LIKE {productName:nvarchar(20)}");

You can use sql types like varchar(size), nvarchar(size), char(size), nchar(size), varchar(MAX), nvarchar(MAX). (If your database does not use this exact types, Dapper will convert them to your database. We pass DbStrings to Dapper and use the hints above to define if they IsAnsi and IsFixedLength.

nvarchar and nchar are unicode strings, while varchar and char are ansi strings.
nvarchar and varchar are variable-length strings, while nchar and char are fixed-length strings.

IN lists

Dapper allows us to use IN lists magically. And it also works with our string interpolation:

varq=cn.QueryBuilder($@" SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] FROM [Product] p INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");varcategories=newstring[]{"Components","Clothing","Acessories"};q.Append($"WHERE c.[Name] IN {categories}");

Fluent API (Chained-methods)

For those who like method-chaining guidance (or for those who allow end-users to build their own queries), there's a Fluent API which allows you to build queries step-by-step mimicking dynamic SQL concatenation.

So, basically, instead of starting with a full query and just appending new filters (.Where()), the QueryBuilder will build the whole query for you:

varq=cn.QueryBuilder().Select($"ProductId").Select($"Name").Select($"ListPrice").Select($"Weight").From($"[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy($"ProductId");varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

Or more elaborated:

varq=cn.QueryBuilder().SelectDistinct($"ProductId, Name, ListPrice, Weight").From("[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy("ProductId");

Building joins dynamically using Fluent API:

varcategories=newstring[]{"Components","Clothing","Acessories"};varq=cn.QueryBuilder().SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]").From($"[Product] p").From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]").From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]").Where($"c.[Name] IN {categories}");

There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY).

nameof() and raw strings

For those who like strongly typed queries, you can also use nameof expression, but you have to define format "raw" such that the string is preserved and it's not converted into a @parameter.

varq=cn.QueryBuilder($@" SELECT c.[{nameof(Category.Name):raw}] as [Category],  sc.[{nameof(Subcategory.Name):raw}] as [Subcategory],  p.[{nameof(Product.Name):raw}], p.[ProductNumber]"
FROM [Product] p
INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]INNER JOIN [ProductCategory]c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");

And in case you can use "find references", "rename" (refactor), etc.

Using Type-Safe Filters without QueryBuilder

If for any reason you don't want to use our QueryBuilder, you can still use type-safe dynamic filters:

Dapper.DynamicParametersparms=newDapper.DynamicParameters();varfilters=newFilters(Filters.FiltersType.AND);filters.Add(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});filters.Add(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});stringwhere=filters.BuildFilters(parms);// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// parms contains @p0 as minPrice, @p1 as maxPrice, etc..

Invoking Stored Procedures

// This is basically Dapper, but with a FluentAPI where you can append parameters dynamically.varq=cn.CommandBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]").AddParameter("ReturnValue",dbType:DbType.Int32,direction:ParameterDirection.ReturnValue).AddParameter("ErrorLogID",dbType:DbType.Int32,direction:ParameterDirection.Output).AddParameter("BusinessEntityID",businessEntityID).AddParameter("NationalIDNumber",nationalIDNumber).AddParameter("BirthDate",birthDate).AddParameter("MaritalStatus",maritalStatus).AddParameter("Gender",gender);intaffected=q.Execute(commandType:CommandType.StoredProcedure);intreturnValue=q.Parameters.Get<int>("ReturnValue");

How was life before this library? :-)

Building dynamic filters in Dapper was a little cumbersome / ugly:

varparms=newDynamicParameters();List<string>filters=newList<string>();filters.Add("[Name] LIKE @productName");parms.Add("productName",productName);filters.Add("[CategoryId] = @categoryId");parms.Add("categoryId",categoryId);stringwhere=(filters.Any()?" WHERE "+string.Join(" AND ",filters):"");varproducts=cn.Query<Product>($@" SELECT * FROM [Product]"{where}
ORDER BY[ProductId]",parms);

Now with DapperQueryBuilder it's much easier to write queries with dynamic filters:

varquery=cn.QueryBuilder(@" SELECT * FROM [Product]  /**where**/  ORDER BY [ProductId]").Where($"[Name] LIKE {productName}").Where($"[CategoryId] = {categoryId}");varproducts=query.Query<Product>();

Database Compatibility

QueryBuilder is database agnostic - it should work with any database, because it basically only helps to pass parameters - it does not generate SQL statements (except simple clauses like WHERE, AND, if you're using /**where**/). It was tested with Microsoft SQL Server and with PostgreSQL (using Npgsql driver), and works fine in both.

If your database driver does not accept "at-parameters" (@p0, @p1, etc), then you can just modify InterpolatedStatementParser.AutoGeneratedParameterPrefix:

// Default value is "@p", some database vendors may not accept "@" parametersInterpolatedStatementParser.AutoGeneratedParameterPrefix=":p";stringsearch="%Dinosaur%";varcmd=cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}");// Underlying SQL will be: SELECT * FROM film WHERE title like :p0

PS: Npgsql accepts "at-parameters" (@p0, @p1, etc) and will convert/pass them correctly to PostgreSQL - so you don't need to use this for Npgsql.

Collaborate

This is a brand new project, and your contribution can help a lot.

Would you like to collaborate?

Please submit a pull-request or if you prefer you can create an issue or contact me to discuss your idea.

License

MIT License

About

Dapper Query Builder using String Interpolation and Fluent API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Dapper Query Builder

Dapper Query Builder using String Interpolation and Fluent API

We all love Dapper and how Dapper is a minimalist library.

This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. It's based on 2 fundamentals:

Fundamental 1: String Interpolation instead of manually using DynamicParameters

By using interpolated strings we can pass parameters to Dapper without having to worry about creating and managing DynamicParameters manually.
You can build your queries with interpolated strings, and this library will automatically "parametrize" your values.

(If you just passed an interpolated string to Dapper, you would have to manually sanitize your inputs against SQL-injection attacks, and on top of that your queries wouldn't benefit from cached execution plan).

Instead of writing like this:

varproducts=cn.Query<Product>($@" SELECT * FROM [Product] WHERE [Name] LIKE @productName AND [ProductSubcategoryID] = @subCategoryId ORDER BY [ProductId]",new{productName,subCategoryId});

... you can just write like this:

varproducts=cn.QueryBuilder($@" SELECT * FROM [Product] WHERE [Name] LIKE {productName} AND [ProductSubcategoryID] = {subCategoryId} ORDER BY [ProductId]").Query<Product>;

The underlying query will be fully parametrized ([Name] LIKE @p0 AND [ProductSubcategoryID] = @p1), without risk of SQL-injection, even though it looks like you're just building dynamic sql.

Fundamental 2: Query and Parameters walk side-by-side

QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with your query.
This is a simple concept but it allows us to add new sql clauses (parametrized) in a single statement.

Let's say you're building a query with a variable number of conditions. Instead of appending multiple conditions like this:

vardynamicParams=newDynamicParameters();stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=" AND Name LIKE @productName";dynamicParams.Add("productName",productName);sql+=" AND ProductSubcategoryID = @subCategoryId";dynamicParams.Add("subCategoryId",subCategoryId);varproducts=cn.Query<Product>(sql,dynamicParams);// or like this:stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=$" AND Name LIKE {productName.Replace("'","''")}";sql+=$" AND ProductSubcategoryID = {subCategoryId.Replace("'","''")}";// here is where you pray that you've correctly sanitized inputs against sql-injectionvarproducts=cn.Query<Product>(sql);

... you can just write like this:

varquery=cn.QueryBuilder($"SELECT * FROM [Product] WHERE 1=1");query.Append($"AND Name LIKE {productName}");query.Append($"AND ProductSubcategoryID = {subCategoryId}");varproducts=query.Query<Product>();

QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily.
When you invoke Query, the underlying query and parameters are passed to Dapper.

Quickstart / NuGet Package

  1. Install the NuGet package Dapper-QueryBuilder
  2. Start using like this:
usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varproducts=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [ListPrice] <= {maxPrice} AND [Weight] <= {maxWeight} AND [Name] LIKE {search} ORDER BY ProductId").Query<Product>();

Or building dynamic conditions like this:

usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varq=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1 ");q.AppendLine("AND [ListPrice] <= {maxPrice}");q.AppendLine("AND [Weight] <= {maxWeight}");q.AppendLine("AND [Name] LIKE {search}");q.AppendLine("ORDER BY ProductId");varproducts=q.Query<Product>();

Full Documentation and Extra features

Filters as First-class citizen

As shown above, you'll still write plain SQL, which is what we all love about Dapper.
Since the most common use case for dynamic clauses is adding WHERE parameters, the library offers WHERE filters as a special structure:

  • You can add filters to QueryBuilder using .Where() method, those filters are saved internally
  • When you send your query to Dapper, QueryBuilder will search for a /**where**/ statement in your query and will replace with the filters you defined.

So you can still write your queries on your own, and yet benefit from string interpolation (which is our mojo and charm) and from dynamically building a list of filters.

intmaxPrice=1000;intmaxWeight=15;stringsearch="%Mountain%";varcn=newSqlConnection(connectionString);// You can build the query manually and just use QueryBuilder to replace "where" filters (if any)varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");// You just pass the parameters as if it was an interpolated string, // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe)q.Where($"[ListPrice] <= {maxPrice}");q.Where($"[Weight] <= {maxWeight}");q.Where($"[Name] LIKE {search}");// Query() will automatically build your query and replace your /**where**/ (if any filter was added)varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

If you don't need the WHERE keyword (if you already have other fixed conditions before), you can use /**filters**/ instead:

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [Price]>{minPrice} /**filters**/ ORDER BY ProductId ");

Combining Filters

QueryBuilder contains an internal property called "Filters" which just keeps track of all conditions you've added using .Where() method.
All those conditions by default are combined with AND operator.

If you want to write more complex filters (combining multiple AND/OR filters) we have a typed structure for that, like other query builders do. But differently from other builders, we don't try to reinvent SQL syntax or create a limited abstraction over SQL language, which is powerful, comprehensive, and vendor-specific, so you should still write your raw filters as if they were regular strings, and we do the rest (structuring AND/OR filters, and extracting parameters from interpolated strings):

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");q.Where(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});q.Where(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});varproducts=q.Query<Product>();// Query() will automatically build your SQL query, and will replace your /**where**/ (if any filter was added)// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// it will also pass an underlying DynamicParameters object, with all parameters you passed using string interpolation // (@p0 as minPrice, @p1 as maxPrice, etc..)

Raw command building

If you don't like the "magic" of replacing /**where**/ filters, you can do everything on your own.

// start your basic queryvarq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1");// append whatever statements you need (.Append instead of .Where!)q.Append($"AND [ListPrice] <= {maxPrice}");q.Append($"AND [Weight] <= {maxWeight}");q.Append($"AND [Name] LIKE {search}");q.Append($"ORDER BY ProductId");varproducts=q.Query<Product>();

varchar vs nvarchar

Dapper has an issue with strings because they are assumed to be unicode strings (nvarchar) by default.
That works, but does not give the best performance - in some cases you may prefer to explicitly describe if your strings are unicode or ansi (nvarchar or varchar), and also describe their exact sizes.

Instead of using Dapper DbString class, you can just pass explicit type for your parameters, like this:

// start your basic querystringproductName="Mountain%";varquery=cn.QueryBuilder($@" SELECT * FROM [Production].[Product] p  WHERE [Name] LIKE {productName:nvarchar(20)}");

You can use sql types like varchar(size), nvarchar(size), char(size), nchar(size), varchar(MAX), nvarchar(MAX). (If your database does not use this exact types, Dapper will convert them to your database. We pass DbStrings to Dapper and use the hints above to define if they IsAnsi and IsFixedLength.

nvarchar and nchar are unicode strings, while varchar and char are ansi strings.
nvarchar and varchar are variable-length strings, while nchar and char are fixed-length strings.

IN lists

Dapper allows us to use IN lists magically. And it also works with our string interpolation:

varq=cn.QueryBuilder($@" SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] FROM [Product] p INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");varcategories=newstring[]{"Components","Clothing","Acessories"};q.Append($"WHERE c.[Name] IN {categories}");

Fluent API (Chained-methods)

For those who like method-chaining guidance (or for those who allow end-users to build their own queries), there's a Fluent API which allows you to build queries step-by-step mimicking dynamic SQL concatenation.

So, basically, instead of starting with a full query and just appending new filters (.Where()), the QueryBuilder will build the whole query for you:

varq=cn.QueryBuilder().Select($"ProductId").Select($"Name").Select($"ListPrice").Select($"Weight").From($"[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy($"ProductId");varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

Or more elaborated:

varq=cn.QueryBuilder().SelectDistinct($"ProductId, Name, ListPrice, Weight").From("[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy("ProductId");

Building joins dynamically using Fluent API:

varcategories=newstring[]{"Components","Clothing","Acessories"};varq=cn.QueryBuilder().SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]").From($"[Product] p").From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]").From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]").Where($"c.[Name] IN {categories}");

There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY).

nameof() and raw strings

For those who like strongly typed queries, you can also use nameof expression, but you have to define format "raw" such that the string is preserved and it's not converted into a @parameter.

varq=cn.QueryBuilder($@" SELECT c.[{nameof(Category.Name):raw}] as [Category],  sc.[{nameof(Subcategory.Name):raw}] as [Subcategory],  p.[{nameof(Product.Name):raw}], p.[ProductNumber]"
FROM [Product] p
INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]INNER JOIN [ProductCategory]c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");

And in case you can use "find references", "rename" (refactor), etc.

Using Type-Safe Filters without QueryBuilder

If for any reason you don't want to use our QueryBuilder, you can still use type-safe dynamic filters:

Dapper.DynamicParametersparms=newDapper.DynamicParameters();varfilters=newFilters(Filters.FiltersType.AND);filters.Add(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});filters.Add(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});stringwhere=filters.BuildFilters(parms);// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// parms contains @p0 as minPrice, @p1 as maxPrice, etc..

Invoking Stored Procedures

// This is basically Dapper, but with a FluentAPI where you can append parameters dynamically.varq=cn.CommandBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]").AddParameter("ReturnValue",dbType:DbType.Int32,direction:ParameterDirection.ReturnValue).AddParameter("ErrorLogID",dbType:DbType.Int32,direction:ParameterDirection.Output).AddParameter("BusinessEntityID",businessEntityID).AddParameter("NationalIDNumber",nationalIDNumber).AddParameter("BirthDate",birthDate).AddParameter("MaritalStatus",maritalStatus).AddParameter("Gender",gender);intaffected=q.Execute(commandType:CommandType.StoredProcedure);intreturnValue=q.Parameters.Get<int>("ReturnValue");

How was life before this library? :-)

Building dynamic filters in Dapper was a little cumbersome / ugly:

varparms=newDynamicParameters();List<string>filters=newList<string>();filters.Add("[Name] LIKE @productName");parms.Add("productName",productName);filters.Add("[CategoryId] = @categoryId");parms.Add("categoryId",categoryId);stringwhere=(filters.Any()?" WHERE "+string.Join(" AND ",filters):"");varproducts=cn.Query<Product>($@" SELECT * FROM [Product]"{where}
ORDER BY[ProductId]",parms);

Now with DapperQueryBuilder it's much easier to write queries with dynamic filters:

varquery=cn.QueryBuilder(@" SELECT * FROM [Product]  /**where**/  ORDER BY [ProductId]").Where($"[Name] LIKE {productName}").Where($"[CategoryId] = {categoryId}");varproducts=query.Query<Product>();

Database Compatibility

QueryBuilder is database agnostic - it should work with any database, because it basically only helps to pass parameters - it does not generate SQL statements (except simple clauses like WHERE, AND, if you're using /**where**/). It was tested with Microsoft SQL Server and with PostgreSQL (using Npgsql driver), and works fine in both.

If your database driver does not accept "at-parameters" (@p0, @p1, etc), then you can just modify InterpolatedStatementParser.AutoGeneratedParameterPrefix:

// Default value is "@p", some database vendors may not accept "@" parametersInterpolatedStatementParser.AutoGeneratedParameterPrefix=":p";stringsearch="%Dinosaur%";varcmd=cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}");// Underlying SQL will be: SELECT * FROM film WHERE title like :p0

PS: Npgsql accepts "at-parameters" (@p0, @p1, etc) and will convert/pass them correctly to PostgreSQL - so you don't need to use this for Npgsql.

Collaborate

This is a brand new project, and your contribution can help a lot.

Would you like to collaborate?

Please submit a pull-request or if you prefer you can create an issue or contact me to discuss your idea.

License

MIT License

About

Dapper Query Builder using String Interpolation and Fluent API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Dapper Query Builder

Dapper Query Builder using String Interpolation and Fluent API

We all love Dapper and how Dapper is a minimalist library.

This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. It's based on 2 fundamentals:

Fundamental 1: String Interpolation instead of manually using DynamicParameters

By using interpolated strings we can pass parameters to Dapper without having to worry about creating and managing DynamicParameters manually.
You can build your queries with interpolated strings, and this library will automatically "parametrize" your values.

(If you just passed an interpolated string to Dapper, you would have to manually sanitize your inputs against SQL-injection attacks, and on top of that your queries wouldn't benefit from cached execution plan).

Instead of writing like this:

varproducts=cn.Query<Product>($@" SELECT * FROM [Product] WHERE [Name] LIKE @productName AND [ProductSubcategoryID] = @subCategoryId ORDER BY [ProductId]",new{productName,subCategoryId});

... you can just write like this:

varproducts=cn.QueryBuilder($@" SELECT * FROM [Product] WHERE [Name] LIKE {productName} AND [ProductSubcategoryID] = {subCategoryId} ORDER BY [ProductId]").Query<Product>;

The underlying query will be fully parametrized ([Name] LIKE @p0 AND [ProductSubcategoryID] = @p1), without risk of SQL-injection, even though it looks like you're just building dynamic sql.

Fundamental 2: Query and Parameters walk side-by-side

QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with your query.
This is a simple concept but it allows us to add new sql clauses (parametrized) in a single statement.

Let's say you're building a query with a variable number of conditions. Instead of appending multiple conditions like this:

vardynamicParams=newDynamicParameters();stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=" AND Name LIKE @productName";dynamicParams.Add("productName",productName);sql+=" AND ProductSubcategoryID = @subCategoryId";dynamicParams.Add("subCategoryId",subCategoryId);varproducts=cn.Query<Product>(sql,dynamicParams);// or like this:stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=$" AND Name LIKE {productName.Replace("'","''")}";sql+=$" AND ProductSubcategoryID = {subCategoryId.Replace("'","''")}";// here is where you pray that you've correctly sanitized inputs against sql-injectionvarproducts=cn.Query<Product>(sql);

... you can just write like this:

varquery=cn.QueryBuilder($"SELECT * FROM [Product] WHERE 1=1");query.Append($"AND Name LIKE {productName}");query.Append($"AND ProductSubcategoryID = {subCategoryId}");varproducts=query.Query<Product>();

QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily.
When you invoke Query, the underlying query and parameters are passed to Dapper.

Quickstart / NuGet Package

  1. Install the NuGet package Dapper-QueryBuilder
  2. Start using like this:
usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varproducts=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [ListPrice] <= {maxPrice} AND [Weight] <= {maxWeight} AND [Name] LIKE {search} ORDER BY ProductId").Query<Product>();

Or building dynamic conditions like this:

usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varq=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1 ");q.AppendLine("AND [ListPrice] <= {maxPrice}");q.AppendLine("AND [Weight] <= {maxWeight}");q.AppendLine("AND [Name] LIKE {search}");q.AppendLine("ORDER BY ProductId");varproducts=q.Query<Product>();

Full Documentation and Extra features

Filters as First-class citizen

As shown above, you'll still write plain SQL, which is what we all love about Dapper.
Since the most common use case for dynamic clauses is adding WHERE parameters, the library offers WHERE filters as a special structure:

  • You can add filters to QueryBuilder using .Where() method, those filters are saved internally
  • When you send your query to Dapper, QueryBuilder will search for a /**where**/ statement in your query and will replace with the filters you defined.

So you can still write your queries on your own, and yet benefit from string interpolation (which is our mojo and charm) and from dynamically building a list of filters.

intmaxPrice=1000;intmaxWeight=15;stringsearch="%Mountain%";varcn=newSqlConnection(connectionString);// You can build the query manually and just use QueryBuilder to replace "where" filters (if any)varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");// You just pass the parameters as if it was an interpolated string, // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe)q.Where($"[ListPrice] <= {maxPrice}");q.Where($"[Weight] <= {maxWeight}");q.Where($"[Name] LIKE {search}");// Query() will automatically build your query and replace your /**where**/ (if any filter was added)varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

If you don't need the WHERE keyword (if you already have other fixed conditions before), you can use /**filters**/ instead:

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [Price]>{minPrice} /**filters**/ ORDER BY ProductId ");

Combining Filters

QueryBuilder contains an internal property called "Filters" which just keeps track of all conditions you've added using .Where() method.
All those conditions by default are combined with AND operator.

If you want to write more complex filters (combining multiple AND/OR filters) we have a typed structure for that, like other query builders do. But differently from other builders, we don't try to reinvent SQL syntax or create a limited abstraction over SQL language, which is powerful, comprehensive, and vendor-specific, so you should still write your raw filters as if they were regular strings, and we do the rest (structuring AND/OR filters, and extracting parameters from interpolated strings):

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");q.Where(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});q.Where(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});varproducts=q.Query<Product>();// Query() will automatically build your SQL query, and will replace your /**where**/ (if any filter was added)// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// it will also pass an underlying DynamicParameters object, with all parameters you passed using string interpolation // (@p0 as minPrice, @p1 as maxPrice, etc..)

Raw command building

If you don't like the "magic" of replacing /**where**/ filters, you can do everything on your own.

// start your basic queryvarq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1");// append whatever statements you need (.Append instead of .Where!)q.Append($"AND [ListPrice] <= {maxPrice}");q.Append($"AND [Weight] <= {maxWeight}");q.Append($"AND [Name] LIKE {search}");q.Append($"ORDER BY ProductId");varproducts=q.Query<Product>();

varchar vs nvarchar

Dapper has an issue with strings because they are assumed to be unicode strings (nvarchar) by default.
That works, but does not give the best performance - in some cases you may prefer to explicitly describe if your strings are unicode or ansi (nvarchar or varchar), and also describe their exact sizes.

Instead of using Dapper DbString class, you can just pass explicit type for your parameters, like this:

// start your basic querystringproductName="Mountain%";varquery=cn.QueryBuilder($@" SELECT * FROM [Production].[Product] p  WHERE [Name] LIKE {productName:nvarchar(20)}");

You can use sql types like varchar(size), nvarchar(size), char(size), nchar(size), varchar(MAX), nvarchar(MAX). (If your database does not use this exact types, Dapper will convert them to your database. We pass DbStrings to Dapper and use the hints above to define if they IsAnsi and IsFixedLength.

nvarchar and nchar are unicode strings, while varchar and char are ansi strings.
nvarchar and varchar are variable-length strings, while nchar and char are fixed-length strings.

IN lists

Dapper allows us to use IN lists magically. And it also works with our string interpolation:

varq=cn.QueryBuilder($@" SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] FROM [Product] p INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");varcategories=newstring[]{"Components","Clothing","Acessories"};q.Append($"WHERE c.[Name] IN {categories}");

Fluent API (Chained-methods)

For those who like method-chaining guidance (or for those who allow end-users to build their own queries), there's a Fluent API which allows you to build queries step-by-step mimicking dynamic SQL concatenation.

So, basically, instead of starting with a full query and just appending new filters (.Where()), the QueryBuilder will build the whole query for you:

varq=cn.QueryBuilder().Select($"ProductId").Select($"Name").Select($"ListPrice").Select($"Weight").From($"[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy($"ProductId");varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

Or more elaborated:

varq=cn.QueryBuilder().SelectDistinct($"ProductId, Name, ListPrice, Weight").From("[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy("ProductId");

Building joins dynamically using Fluent API:

varcategories=newstring[]{"Components","Clothing","Acessories"};varq=cn.QueryBuilder().SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]").From($"[Product] p").From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]").From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]").Where($"c.[Name] IN {categories}");

There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY).

nameof() and raw strings

For those who like strongly typed queries, you can also use nameof expression, but you have to define format "raw" such that the string is preserved and it's not converted into a @parameter.

varq=cn.QueryBuilder($@" SELECT c.[{nameof(Category.Name):raw}] as [Category],  sc.[{nameof(Subcategory.Name):raw}] as [Subcategory],  p.[{nameof(Product.Name):raw}], p.[ProductNumber]"
FROM [Product] p
INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]INNER JOIN [ProductCategory]c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");

And in case you can use "find references", "rename" (refactor), etc.

Using Type-Safe Filters without QueryBuilder

If for any reason you don't want to use our QueryBuilder, you can still use type-safe dynamic filters:

Dapper.DynamicParametersparms=newDapper.DynamicParameters();varfilters=newFilters(Filters.FiltersType.AND);filters.Add(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});filters.Add(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});stringwhere=filters.BuildFilters(parms);// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// parms contains @p0 as minPrice, @p1 as maxPrice, etc..

Invoking Stored Procedures

// This is basically Dapper, but with a FluentAPI where you can append parameters dynamically.varq=cn.CommandBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]").AddParameter("ReturnValue",dbType:DbType.Int32,direction:ParameterDirection.ReturnValue).AddParameter("ErrorLogID",dbType:DbType.Int32,direction:ParameterDirection.Output).AddParameter("BusinessEntityID",businessEntityID).AddParameter("NationalIDNumber",nationalIDNumber).AddParameter("BirthDate",birthDate).AddParameter("MaritalStatus",maritalStatus).AddParameter("Gender",gender);intaffected=q.Execute(commandType:CommandType.StoredProcedure);intreturnValue=q.Parameters.Get<int>("ReturnValue");

How was life before this library? :-)

Building dynamic filters in Dapper was a little cumbersome / ugly:

varparms=newDynamicParameters();List<string>filters=newList<string>();filters.Add("[Name] LIKE @productName");parms.Add("productName",productName);filters.Add("[CategoryId] = @categoryId");parms.Add("categoryId",categoryId);stringwhere=(filters.Any()?" WHERE "+string.Join(" AND ",filters):"");varproducts=cn.Query<Product>($@" SELECT * FROM [Product]"{where}
ORDER BY[ProductId]",parms);

Now with DapperQueryBuilder it's much easier to write queries with dynamic filters:

varquery=cn.QueryBuilder(@" SELECT * FROM [Product]  /**where**/  ORDER BY [ProductId]").Where($"[Name] LIKE {productName}").Where($"[CategoryId] = {categoryId}");varproducts=query.Query<Product>();

Database Compatibility

QueryBuilder is database agnostic - it should work with any database, because it basically only helps to pass parameters - it does not generate SQL statements (except simple clauses like WHERE, AND, if you're using /**where**/). It was tested with Microsoft SQL Server and with PostgreSQL (using Npgsql driver), and works fine in both.

If your database driver does not accept "at-parameters" (@p0, @p1, etc), then you can just modify InterpolatedStatementParser.AutoGeneratedParameterPrefix:

// Default value is "@p", some database vendors may not accept "@" parametersInterpolatedStatementParser.AutoGeneratedParameterPrefix=":p";stringsearch="%Dinosaur%";varcmd=cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}");// Underlying SQL will be: SELECT * FROM film WHERE title like :p0

PS: Npgsql accepts "at-parameters" (@p0, @p1, etc) and will convert/pass them correctly to PostgreSQL - so you don't need to use this for Npgsql.

Collaborate

This is a brand new project, and your contribution can help a lot.

Would you like to collaborate?

Please submit a pull-request or if you prefer you can create an issue or contact me to discuss your idea.

License

MIT License

About

Dapper Query Builder using String Interpolation and Fluent API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Dapper Query Builder

Dapper Query Builder using String Interpolation and Fluent API

We all love Dapper and how Dapper is a minimalist library.

This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. It's based on 2 fundamentals:

Fundamental 1: String Interpolation instead of manually using DynamicParameters

By using interpolated strings we can pass parameters to Dapper without having to worry about creating and managing DynamicParameters manually.
You can build your queries with interpolated strings, and this library will automatically "parametrize" your values.

(If you just passed an interpolated string to Dapper, you would have to manually sanitize your inputs against SQL-injection attacks, and on top of that your queries wouldn't benefit from cached execution plan).

Instead of writing like this:

varproducts=cn.Query<Product>($@" SELECT * FROM [Product] WHERE [Name] LIKE @productName AND [ProductSubcategoryID] = @subCategoryId ORDER BY [ProductId]",new{productName,subCategoryId});

... you can just write like this:

varproducts=cn.QueryBuilder($@" SELECT * FROM [Product] WHERE [Name] LIKE {productName} AND [ProductSubcategoryID] = {subCategoryId} ORDER BY [ProductId]").Query<Product>;

The underlying query will be fully parametrized ([Name] LIKE @p0 AND [ProductSubcategoryID] = @p1), without risk of SQL-injection, even though it looks like you're just building dynamic sql.

Fundamental 2: Query and Parameters walk side-by-side

QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with your query.
This is a simple concept but it allows us to add new sql clauses (parametrized) in a single statement.

Let's say you're building a query with a variable number of conditions. Instead of appending multiple conditions like this:

vardynamicParams=newDynamicParameters();stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=" AND Name LIKE @productName";dynamicParams.Add("productName",productName);sql+=" AND ProductSubcategoryID = @subCategoryId";dynamicParams.Add("subCategoryId",subCategoryId);varproducts=cn.Query<Product>(sql,dynamicParams);// or like this:stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=$" AND Name LIKE {productName.Replace("'","''")}";sql+=$" AND ProductSubcategoryID = {subCategoryId.Replace("'","''")}";// here is where you pray that you've correctly sanitized inputs against sql-injectionvarproducts=cn.Query<Product>(sql);

... you can just write like this:

varquery=cn.QueryBuilder($"SELECT * FROM [Product] WHERE 1=1");query.Append($"AND Name LIKE {productName}");query.Append($"AND ProductSubcategoryID = {subCategoryId}");varproducts=query.Query<Product>();

QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily.
When you invoke Query, the underlying query and parameters are passed to Dapper.

Quickstart / NuGet Package

  1. Install the NuGet package Dapper-QueryBuilder
  2. Start using like this:
usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varproducts=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [ListPrice] <= {maxPrice} AND [Weight] <= {maxWeight} AND [Name] LIKE {search} ORDER BY ProductId").Query<Product>();

Or building dynamic conditions like this:

usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varq=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1 ");q.AppendLine("AND [ListPrice] <= {maxPrice}");q.AppendLine("AND [Weight] <= {maxWeight}");q.AppendLine("AND [Name] LIKE {search}");q.AppendLine("ORDER BY ProductId");varproducts=q.Query<Product>();

Full Documentation and Extra features

Filters as First-class citizen

As shown above, you'll still write plain SQL, which is what we all love about Dapper.
Since the most common use case for dynamic clauses is adding WHERE parameters, the library offers WHERE filters as a special structure:

  • You can add filters to QueryBuilder using .Where() method, those filters are saved internally
  • When you send your query to Dapper, QueryBuilder will search for a /**where**/ statement in your query and will replace with the filters you defined.

So you can still write your queries on your own, and yet benefit from string interpolation (which is our mojo and charm) and from dynamically building a list of filters.

intmaxPrice=1000;intmaxWeight=15;stringsearch="%Mountain%";varcn=newSqlConnection(connectionString);// You can build the query manually and just use QueryBuilder to replace "where" filters (if any)varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");// You just pass the parameters as if it was an interpolated string, // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe)q.Where($"[ListPrice] <= {maxPrice}");q.Where($"[Weight] <= {maxWeight}");q.Where($"[Name] LIKE {search}");// Query() will automatically build your query and replace your /**where**/ (if any filter was added)varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

If you don't need the WHERE keyword (if you already have other fixed conditions before), you can use /**filters**/ instead:

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [Price]>{minPrice} /**filters**/ ORDER BY ProductId ");

Combining Filters

QueryBuilder contains an internal property called "Filters" which just keeps track of all conditions you've added using .Where() method.
All those conditions by default are combined with AND operator.

If you want to write more complex filters (combining multiple AND/OR filters) we have a typed structure for that, like other query builders do. But differently from other builders, we don't try to reinvent SQL syntax or create a limited abstraction over SQL language, which is powerful, comprehensive, and vendor-specific, so you should still write your raw filters as if they were regular strings, and we do the rest (structuring AND/OR filters, and extracting parameters from interpolated strings):

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");q.Where(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});q.Where(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});varproducts=q.Query<Product>();// Query() will automatically build your SQL query, and will replace your /**where**/ (if any filter was added)// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// it will also pass an underlying DynamicParameters object, with all parameters you passed using string interpolation // (@p0 as minPrice, @p1 as maxPrice, etc..)

Raw command building

If you don't like the "magic" of replacing /**where**/ filters, you can do everything on your own.

// start your basic queryvarq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1");// append whatever statements you need (.Append instead of .Where!)q.Append($"AND [ListPrice] <= {maxPrice}");q.Append($"AND [Weight] <= {maxWeight}");q.Append($"AND [Name] LIKE {search}");q.Append($"ORDER BY ProductId");varproducts=q.Query<Product>();

varchar vs nvarchar

Dapper has an issue with strings because they are assumed to be unicode strings (nvarchar) by default.
That works, but does not give the best performance - in some cases you may prefer to explicitly describe if your strings are unicode or ansi (nvarchar or varchar), and also describe their exact sizes.

Instead of using Dapper DbString class, you can just pass explicit type for your parameters, like this:

// start your basic querystringproductName="Mountain%";varquery=cn.QueryBuilder($@" SELECT * FROM [Production].[Product] p  WHERE [Name] LIKE {productName:nvarchar(20)}");

You can use sql types like varchar(size), nvarchar(size), char(size), nchar(size), varchar(MAX), nvarchar(MAX). (If your database does not use this exact types, Dapper will convert them to your database. We pass DbStrings to Dapper and use the hints above to define if they IsAnsi and IsFixedLength.

nvarchar and nchar are unicode strings, while varchar and char are ansi strings.
nvarchar and varchar are variable-length strings, while nchar and char are fixed-length strings.

IN lists

Dapper allows us to use IN lists magically. And it also works with our string interpolation:

varq=cn.QueryBuilder($@" SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] FROM [Product] p INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");varcategories=newstring[]{"Components","Clothing","Acessories"};q.Append($"WHERE c.[Name] IN {categories}");

Fluent API (Chained-methods)

For those who like method-chaining guidance (or for those who allow end-users to build their own queries), there's a Fluent API which allows you to build queries step-by-step mimicking dynamic SQL concatenation.

So, basically, instead of starting with a full query and just appending new filters (.Where()), the QueryBuilder will build the whole query for you:

varq=cn.QueryBuilder().Select($"ProductId").Select($"Name").Select($"ListPrice").Select($"Weight").From($"[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy($"ProductId");varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

Or more elaborated:

varq=cn.QueryBuilder().SelectDistinct($"ProductId, Name, ListPrice, Weight").From("[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy("ProductId");

Building joins dynamically using Fluent API:

varcategories=newstring[]{"Components","Clothing","Acessories"};varq=cn.QueryBuilder().SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]").From($"[Product] p").From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]").From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]").Where($"c.[Name] IN {categories}");

There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY).

nameof() and raw strings

For those who like strongly typed queries, you can also use nameof expression, but you have to define format "raw" such that the string is preserved and it's not converted into a @parameter.

varq=cn.QueryBuilder($@" SELECT c.[{nameof(Category.Name):raw}] as [Category],  sc.[{nameof(Subcategory.Name):raw}] as [Subcategory],  p.[{nameof(Product.Name):raw}], p.[ProductNumber]"
FROM [Product] p
INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]INNER JOIN [ProductCategory]c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");

And in case you can use "find references", "rename" (refactor), etc.

Using Type-Safe Filters without QueryBuilder

If for any reason you don't want to use our QueryBuilder, you can still use type-safe dynamic filters:

Dapper.DynamicParametersparms=newDapper.DynamicParameters();varfilters=newFilters(Filters.FiltersType.AND);filters.Add(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});filters.Add(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});stringwhere=filters.BuildFilters(parms);// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// parms contains @p0 as minPrice, @p1 as maxPrice, etc..

Invoking Stored Procedures

// This is basically Dapper, but with a FluentAPI where you can append parameters dynamically.varq=cn.CommandBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]").AddParameter("ReturnValue",dbType:DbType.Int32,direction:ParameterDirection.ReturnValue).AddParameter("ErrorLogID",dbType:DbType.Int32,direction:ParameterDirection.Output).AddParameter("BusinessEntityID",businessEntityID).AddParameter("NationalIDNumber",nationalIDNumber).AddParameter("BirthDate",birthDate).AddParameter("MaritalStatus",maritalStatus).AddParameter("Gender",gender);intaffected=q.Execute(commandType:CommandType.StoredProcedure);intreturnValue=q.Parameters.Get<int>("ReturnValue");

How was life before this library? :-)

Building dynamic filters in Dapper was a little cumbersome / ugly:

varparms=newDynamicParameters();List<string>filters=newList<string>();filters.Add("[Name] LIKE @productName");parms.Add("productName",productName);filters.Add("[CategoryId] = @categoryId");parms.Add("categoryId",categoryId);stringwhere=(filters.Any()?" WHERE "+string.Join(" AND ",filters):"");varproducts=cn.Query<Product>($@" SELECT * FROM [Product]"{where}
ORDER BY[ProductId]",parms);

Now with DapperQueryBuilder it's much easier to write queries with dynamic filters:

varquery=cn.QueryBuilder(@" SELECT * FROM [Product]  /**where**/  ORDER BY [ProductId]").Where($"[Name] LIKE {productName}").Where($"[CategoryId] = {categoryId}");varproducts=query.Query<Product>();

Database Compatibility

QueryBuilder is database agnostic - it should work with any database, because it basically only helps to pass parameters - it does not generate SQL statements (except simple clauses like WHERE, AND, if you're using /**where**/). It was tested with Microsoft SQL Server and with PostgreSQL (using Npgsql driver), and works fine in both.

If your database driver does not accept "at-parameters" (@p0, @p1, etc), then you can just modify InterpolatedStatementParser.AutoGeneratedParameterPrefix:

// Default value is "@p", some database vendors may not accept "@" parametersInterpolatedStatementParser.AutoGeneratedParameterPrefix=":p";stringsearch="%Dinosaur%";varcmd=cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}");// Underlying SQL will be: SELECT * FROM film WHERE title like :p0

PS: Npgsql accepts "at-parameters" (@p0, @p1, etc) and will convert/pass them correctly to PostgreSQL - so you don't need to use this for Npgsql.

Collaborate

This is a brand new project, and your contribution can help a lot.

Would you like to collaborate?

Please submit a pull-request or if you prefer you can create an issue or contact me to discuss your idea.

License

MIT License

About

Dapper Query Builder using String Interpolation and Fluent API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Dapper Query Builder

Dapper Query Builder using String Interpolation and Fluent API

We all love Dapper and how Dapper is a minimalist library.

This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. It's based on 2 fundamentals:

Fundamental 1: String Interpolation instead of manually using DynamicParameters

By using interpolated strings we can pass parameters to Dapper without having to worry about creating and managing DynamicParameters manually.
You can build your queries with interpolated strings, and this library will automatically "parametrize" your values.

(If you just passed an interpolated string to Dapper, you would have to manually sanitize your inputs against SQL-injection attacks, and on top of that your queries wouldn't benefit from cached execution plan).

Instead of writing like this:

varproducts=cn.Query<Product>($@" SELECT * FROM [Product] WHERE [Name] LIKE @productName AND [ProductSubcategoryID] = @subCategoryId ORDER BY [ProductId]",new{productName,subCategoryId});

... you can just write like this:

varproducts=cn.QueryBuilder($@" SELECT * FROM [Product] WHERE [Name] LIKE {productName} AND [ProductSubcategoryID] = {subCategoryId} ORDER BY [ProductId]").Query<Product>;

The underlying query will be fully parametrized ([Name] LIKE @p0 AND [ProductSubcategoryID] = @p1), without risk of SQL-injection, even though it looks like you're just building dynamic sql.

Fundamental 2: Query and Parameters walk side-by-side

QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with your query.
This is a simple concept but it allows us to add new sql clauses (parametrized) in a single statement.

Let's say you're building a query with a variable number of conditions. Instead of appending multiple conditions like this:

vardynamicParams=newDynamicParameters();stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=" AND Name LIKE @productName";dynamicParams.Add("productName",productName);sql+=" AND ProductSubcategoryID = @subCategoryId";dynamicParams.Add("subCategoryId",subCategoryId);varproducts=cn.Query<Product>(sql,dynamicParams);// or like this:stringsql="SELECT * FROM [Product] WHERE 1=1";sql+=$" AND Name LIKE {productName.Replace("'","''")}";sql+=$" AND ProductSubcategoryID = {subCategoryId.Replace("'","''")}";// here is where you pray that you've correctly sanitized inputs against sql-injectionvarproducts=cn.Query<Product>(sql);

... you can just write like this:

varquery=cn.QueryBuilder($"SELECT * FROM [Product] WHERE 1=1");query.Append($"AND Name LIKE {productName}");query.Append($"AND ProductSubcategoryID = {subCategoryId}");varproducts=query.Query<Product>();

QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily.
When you invoke Query, the underlying query and parameters are passed to Dapper.

Quickstart / NuGet Package

  1. Install the NuGet package Dapper-QueryBuilder
  2. Start using like this:
usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varproducts=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [ListPrice] <= {maxPrice} AND [Weight] <= {maxWeight} AND [Name] LIKE {search} ORDER BY ProductId").Query<Product>();

Or building dynamic conditions like this:

usingDapperQueryBuilder;// ...cn=newSqlConnection(connectionString);varq=cn.QueryBuilder($@" SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1 ");q.AppendLine("AND [ListPrice] <= {maxPrice}");q.AppendLine("AND [Weight] <= {maxWeight}");q.AppendLine("AND [Name] LIKE {search}");q.AppendLine("ORDER BY ProductId");varproducts=q.Query<Product>();

Full Documentation and Extra features

Filters as First-class citizen

As shown above, you'll still write plain SQL, which is what we all love about Dapper.
Since the most common use case for dynamic clauses is adding WHERE parameters, the library offers WHERE filters as a special structure:

  • You can add filters to QueryBuilder using .Where() method, those filters are saved internally
  • When you send your query to Dapper, QueryBuilder will search for a /**where**/ statement in your query and will replace with the filters you defined.

So you can still write your queries on your own, and yet benefit from string interpolation (which is our mojo and charm) and from dynamically building a list of filters.

intmaxPrice=1000;intmaxWeight=15;stringsearch="%Mountain%";varcn=newSqlConnection(connectionString);// You can build the query manually and just use QueryBuilder to replace "where" filters (if any)varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");// You just pass the parameters as if it was an interpolated string, // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe)q.Where($"[ListPrice] <= {maxPrice}");q.Where($"[Weight] <= {maxWeight}");q.Where($"[Name] LIKE {search}");// Query() will automatically build your query and replace your /**where**/ (if any filter was added)varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

If you don't need the WHERE keyword (if you already have other fixed conditions before), you can use /**filters**/ instead:

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE [Price]>{minPrice} /**filters**/ ORDER BY ProductId ");

Combining Filters

QueryBuilder contains an internal property called "Filters" which just keeps track of all conditions you've added using .Where() method.
All those conditions by default are combined with AND operator.

If you want to write more complex filters (combining multiple AND/OR filters) we have a typed structure for that, like other query builders do. But differently from other builders, we don't try to reinvent SQL syntax or create a limited abstraction over SQL language, which is powerful, comprehensive, and vendor-specific, so you should still write your raw filters as if they were regular strings, and we do the rest (structuring AND/OR filters, and extracting parameters from interpolated strings):

varq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] /**where**/ ORDER BY ProductId ");q.Where(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});q.Where(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});varproducts=q.Query<Product>();// Query() will automatically build your SQL query, and will replace your /**where**/ (if any filter was added)// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// it will also pass an underlying DynamicParameters object, with all parameters you passed using string interpolation // (@p0 as minPrice, @p1 as maxPrice, etc..)

Raw command building

If you don't like the "magic" of replacing /**where**/ filters, you can do everything on your own.

// start your basic queryvarq=cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM [Product] WHERE 1=1");// append whatever statements you need (.Append instead of .Where!)q.Append($"AND [ListPrice] <= {maxPrice}");q.Append($"AND [Weight] <= {maxWeight}");q.Append($"AND [Name] LIKE {search}");q.Append($"ORDER BY ProductId");varproducts=q.Query<Product>();

varchar vs nvarchar

Dapper has an issue with strings because they are assumed to be unicode strings (nvarchar) by default.
That works, but does not give the best performance - in some cases you may prefer to explicitly describe if your strings are unicode or ansi (nvarchar or varchar), and also describe their exact sizes.

Instead of using Dapper DbString class, you can just pass explicit type for your parameters, like this:

// start your basic querystringproductName="Mountain%";varquery=cn.QueryBuilder($@" SELECT * FROM [Production].[Product] p  WHERE [Name] LIKE {productName:nvarchar(20)}");

You can use sql types like varchar(size), nvarchar(size), char(size), nchar(size), varchar(MAX), nvarchar(MAX). (If your database does not use this exact types, Dapper will convert them to your database. We pass DbStrings to Dapper and use the hints above to define if they IsAnsi and IsFixedLength.

nvarchar and nchar are unicode strings, while varchar and char are ansi strings.
nvarchar and varchar are variable-length strings, while nchar and char are fixed-length strings.

IN lists

Dapper allows us to use IN lists magically. And it also works with our string interpolation:

varq=cn.QueryBuilder($@" SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] FROM [Product] p INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");varcategories=newstring[]{"Components","Clothing","Acessories"};q.Append($"WHERE c.[Name] IN {categories}");

Fluent API (Chained-methods)

For those who like method-chaining guidance (or for those who allow end-users to build their own queries), there's a Fluent API which allows you to build queries step-by-step mimicking dynamic SQL concatenation.

So, basically, instead of starting with a full query and just appending new filters (.Where()), the QueryBuilder will build the whole query for you:

varq=cn.QueryBuilder().Select($"ProductId").Select($"Name").Select($"ListPrice").Select($"Weight").From($"[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy($"ProductId");varproducts=q.Query<Product>();

You would get this query:

SELECT ProductId, Name, ListPrice, Weight
FROM [Product]
WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2
ORDER BY ProductId

Or more elaborated:

varq=cn.QueryBuilder().SelectDistinct($"ProductId, Name, ListPrice, Weight").From("[Product]").Where($"[ListPrice] <= {maxPrice}").Where($"[Weight] <= {maxWeight}").Where($"[Name] LIKE {search}").OrderBy("ProductId");

Building joins dynamically using Fluent API:

varcategories=newstring[]{"Components","Clothing","Acessories"};varq=cn.QueryBuilder().SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]").From($"[Product] p").From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]").From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]").Where($"c.[Name] IN {categories}");

There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY).

nameof() and raw strings

For those who like strongly typed queries, you can also use nameof expression, but you have to define format "raw" such that the string is preserved and it's not converted into a @parameter.

varq=cn.QueryBuilder($@" SELECT c.[{nameof(Category.Name):raw}] as [Category],  sc.[{nameof(Subcategory.Name):raw}] as [Subcategory],  p.[{nameof(Product.Name):raw}], p.[ProductNumber]"
FROM [Product] p
INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]INNER JOIN [ProductCategory]c ON sc.[ProductCategoryID]=c.[ProductCategoryID]");

And in case you can use "find references", "rename" (refactor), etc.

Using Type-Safe Filters without QueryBuilder

If for any reason you don't want to use our QueryBuilder, you can still use type-safe dynamic filters:

Dapper.DynamicParametersparms=newDapper.DynamicParameters();varfilters=newFilters(Filters.FiltersType.AND);filters.Add(newFilters(){newFilter($"[ListPrice] >= {minPrice}"),newFilter($"[ListPrice] <= {maxPrice}")});filters.Add(newFilters(Filters.FiltersType.OR){newFilter($"[Weight] <= {maxWeight}"),newFilter($"[Name] LIKE {search}")});stringwhere=filters.BuildFilters(parms);// "WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"// parms contains @p0 as minPrice, @p1 as maxPrice, etc..

Invoking Stored Procedures

// This is basically Dapper, but with a FluentAPI where you can append parameters dynamically.varq=cn.CommandBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]").AddParameter("ReturnValue",dbType:DbType.Int32,direction:ParameterDirection.ReturnValue).AddParameter("ErrorLogID",dbType:DbType.Int32,direction:ParameterDirection.Output).AddParameter("BusinessEntityID",businessEntityID).AddParameter("NationalIDNumber",nationalIDNumber).AddParameter("BirthDate",birthDate).AddParameter("MaritalStatus",maritalStatus).AddParameter("Gender",gender);intaffected=q.Execute(commandType:CommandType.StoredProcedure);intreturnValue=q.Parameters.Get<int>("ReturnValue");

How was life before this library? :-)

Building dynamic filters in Dapper was a little cumbersome / ugly:

varparms=newDynamicParameters();List<string>filters=newList<string>();filters.Add("[Name] LIKE @productName");parms.Add("productName",productName);filters.Add("[CategoryId] = @categoryId");parms.Add("categoryId",categoryId);stringwhere=(filters.Any()?" WHERE "+string.Join(" AND ",filters):"");varproducts=cn.Query<Product>($@" SELECT * FROM [Product]"{where}
ORDER BY[ProductId]",parms);

Now with DapperQueryBuilder it's much easier to write queries with dynamic filters:

varquery=cn.QueryBuilder(@" SELECT * FROM [Product]  /**where**/  ORDER BY [ProductId]").Where($"[Name] LIKE {productName}").Where($"[CategoryId] = {categoryId}");varproducts=query.Query<Product>();

Database Compatibility

QueryBuilder is database agnostic - it should work with any database, because it basically only helps to pass parameters - it does not generate SQL statements (except simple clauses like WHERE, AND, if you're using /**where**/). It was tested with Microsoft SQL Server and with PostgreSQL (using Npgsql driver), and works fine in both.

If your database driver does not accept "at-parameters" (@p0, @p1, etc), then you can just modify InterpolatedStatementParser.AutoGeneratedParameterPrefix:

// Default value is "@p", some database vendors may not accept "@" parametersInterpolatedStatementParser.AutoGeneratedParameterPrefix=":p";stringsearch="%Dinosaur%";varcmd=cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}");// Underlying SQL will be: SELECT * FROM film WHERE title like :p0

PS: Npgsql accepts "at-parameters" (@p0, @p1, etc) and will convert/pass them correctly to PostgreSQL - so you don't need to use this for Npgsql.

Collaborate

This is a brand new project, and your contribution can help a lot.

Would you like to collaborate?

Please submit a pull-request or if you prefer you can create an issue or contact me to discuss your idea.

License

MIT License

About

Dapper Query Builder using String Interpolation and Fluent API

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages