├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md └── src ├── .editorconfig ├── DapperQueryBuilder.StrongName ├── DapperQueryBuilder.StrongName.csproj └── DapperQueryBuilder.StrongName.nuspec ├── DapperQueryBuilder.Tests ├── CommandBuilderTests.cs ├── DapperQueryBuilder.Tests.csproj ├── ExplicitTypeTests.cs ├── FluentQueryBuilderTests.cs ├── Helpers │ ├── UnitTestsDbCommand.cs │ └── UnitTestsDbConnection.cs ├── MySQLTests.cs ├── PostgreSQLTests.cs ├── QueryBuilderTests.cs ├── Setup-MSSQL.sql ├── Setup-MySQL.sql ├── TestHelper.cs └── TestSettings.json ├── DapperQueryBuilder.sln ├── DapperQueryBuilder ├── Dapper-QueryBuilder.nuspec ├── DapperQueryBuilder.csproj ├── GlobalSuppressions.cs ├── IDapperSqlBuilderExtensions.cs ├── IDapperSqlCommand.cs ├── IDapperSqlCommandExtensions.cs ├── IDbConnectionExtensions.cs ├── ImmutableDapperCommand.cs ├── InterpolatedSqlDapperOptions.cs ├── NuGetReadMe.md ├── SqlBuilderFactory.cs ├── SqlBuilders │ ├── FluentQueryBuilder │ │ ├── FluentQueryBuilder.cs │ │ ├── IDbConnectionExtensions.cs │ │ └── IFluentQueryBuilder.cs │ ├── IDapperSqlBuilder.cs │ ├── InsertUpdateBuilder │ │ ├── IDbConnectionExtensions.cs │ │ ├── IInsertUpdateBuilder.cs │ │ ├── InsertUpdateBuilder.cs │ │ └── InsertUpdateBuilder{U,RB,R}.cs │ ├── QueryBuilder │ │ ├── Filter.cs │ │ ├── Filters.cs │ │ ├── IQueryBuilder.cs │ │ ├── QueryBuilder.cs │ │ └── QueryBuilder{U,RB,R}.cs │ └── SqlBuilder │ │ ├── ISqlBuilder.cs │ │ ├── SqlBuilder.cs │ │ └── SqlBuilder{U,RB,R}.cs └── SqlParameters │ ├── ParametersDictionary.cs │ └── SqlParameterMapper.cs ├── build.ps1 └── debug.snk /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Drizin] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | /src/DapperQueryBuilder/DapperQueryBuilder.xml 352 | /src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.xml 353 | /src/release.snk 354 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rick Drizin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Nuget](https://img.shields.io/nuget/v/Dapper-QueryBuilder?label=Dapper-QueryBuilder)](https://www.nuget.org/packages/Dapper-QueryBuilder) 2 | [![Downloads](https://img.shields.io/nuget/dt/Dapper-QueryBuilder.svg)](https://www.nuget.org/packages/Dapper-QueryBuilder) 3 | [![Nuget](https://img.shields.io/nuget/v/DapperQueryBuilder.StrongName?label=DapperQueryBuilder.StrongName)](https://www.nuget.org/packages/DapperQueryBuilder.StrongName) 4 | [![Downloads](https://img.shields.io/nuget/dt/DapperQueryBuilder.StrongName.svg)](https://www.nuget.org/packages/DapperQueryBuilder.StrongName) 5 | 6 | # **Important Notice** 7 | 8 | DapperQueryBuilder was fully rewritten into a new library [InterpolatedSql](https://github.com/Drizin/InterpolatedSql/) which is faster, much more extensible, and does not depend on Dapper (you can use our magic string interpolation with any ORM or Micro-ORM or even bare ADO.NET). 9 | The Dapper-specific features (Dapper extensions and mapping of Dapper types) were moved into [InterpolatedSql.Dapper](https://github.com/Drizin/InterpolatedSql/tree/main/src/InterpolatedSql.Dapper), which is our "batteries-included library", and it still supports all databases supported by Dapper. 10 | 11 | The current release of DapperQueryBuilder (2.x.x) is just a copy of InterpolatedSql.Dapper with some minor facades added for backward compatibility (e.g. you can still use the old `DapperQueryBuilder` namespace). For long-term support (new features and bug fixes) please consider moving to [InterpolatedSql.Dapper](https://github.com/Drizin/InterpolatedSql/tree/main/src/InterpolatedSql.Dapper), which will get more frequent updates. 12 | 13 | Please consider starring :star: both repositories. 14 | 15 | # Dapper Query Builder 16 | 17 | **Dapper Query Builder using String Interpolation and Fluent API** 18 | 19 | We all love Dapper and how Dapper is a minimalist library. 20 | 21 | This library is a tiny wrapper around Dapper to help manual building of dynamic SQL queries and commands. It's based on 2 fundamentals: 22 | 23 | ## Fundamental 1: Parameters are passed using String Interpolation (but it's safe against SQL injection!) 24 | 25 | By using interpolated strings we can pass parameters directly (embedded in the query) without having to use anonymous objects and without worrying about matching the property names with the SQL parameters. We can just build our queries with regular string interpolation and this library **will automatically "parameterize" our interpolated objects (sql-injection safe)**. 26 | 27 | With plain Dapper we would write a parameterized query like this: 28 | 29 | ```cs 30 | string productName = "%Computer%"; 31 | int subCategoryId = 10; 32 | 33 | // Note that the SQL parameter names (@productName and @subCategoryId)... 34 | var products = cn 35 | .Query($@" 36 | SELECT * FROM Product 37 | WHERE 38 | Name LIKE @productName 39 | AND ProductSubcategoryID = @subCategoryId 40 | ORDER BY ProductId", 41 | new { productName, subCategoryId }); // ... must match the anonymous object 42 | ``` 43 | 44 | **With Dapper Query Builder we can just embed variables inside the query:** 45 | ```cs 46 | string productName = "%Computer%"; 47 | int subCategoryId = 10; 48 | 49 | var products = cn 50 | .QueryBuilder($@" 51 | SELECT * FROM Product 52 | WHERE 53 | Name LIKE {productName} 54 | AND ProductSubcategoryID = {subCategoryId} 55 | ORDER BY ProductId" 56 | ).Query(); 57 | ``` 58 | When `.Query()` is invoked `QueryBuilder` will basically invoke Dapper equivalent method (`Query()`) and pass a fully parameterized query (without risk of SQL-injection) even though it looks like you're just building dynamic sql. 59 | 60 | Dapper would receive a fully parameterized query, but without the risk of having mismatches in the names or number of parameters. Dapper would get this sql: 61 | 62 | ```sql 63 | SELECT * FROM Product 64 | WHERE 65 | Name LIKE @p0 66 | AND ProductSubcategoryID = @p1 67 | ORDER BY ProductId 68 | ``` 69 | and these parameters: ```new { p0 = productName, p1 = subCategoryId } ``` 70 | 71 | 72 | 73 | ## Fundamental 2: Query and Parameters walk side-by-side 74 | 75 | QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with our query. 76 | This is a simple concept but it allows us to dynamically add new parameterized SQL clauses/conditions in a single statement. 77 | 78 | This is how we would build a query with a variable number of conditions using plain Dapper: 79 | 80 | ```cs 81 | var dynamicParams = new DynamicParameters(); 82 | string sql = "SELECT * FROM Product WHERE 1=1"; 83 | sql += " AND Name LIKE @productName"; 84 | dynamicParams.Add("productName", productName); 85 | sql += " AND ProductSubcategoryID = @subCategoryId"; 86 | dynamicParams.Add("subCategoryId", subCategoryId); 87 | var products = cn.Query(sql, dynamicParams); 88 | ``` 89 | 90 | 91 | **With Dapper Query Builder the SQL statement and the associated Parameters are kept together**, making it easy to append dynamic conditions: 92 | ```cs 93 | var query = cn.QueryBuilder($"SELECT * FROM Product WHERE 1=1"); 94 | query += $"AND Name LIKE {productName}"; 95 | query += $"AND ProductSubcategoryID = {subCategoryId}"; 96 | var products = query.Query(); 97 | ``` 98 | 99 | Our classes (`QueryBuilder` and `CommandBuilder`) wrap the SQL statement and the associated Parameters, and when we invoke the Query (or run the Command) the underlying statement and parameters are just passed to Dapper. So we don't have to keep statement and parameters separated and we don't have to manually use `DynamicParameters`. 100 | 101 | 102 | 103 | # Quickstart / NuGet Package 104 | 105 | 1. Install the [NuGet package Dapper-QueryBuilder](https://www.nuget.org/packages/Dapper-QueryBuilder) (don't forget the dash to get the right package!) or [NuGet package DapperQueryBuilder.StrongName](https://www.nuget.org/packages/DapperQueryBuilder.StrongName) 106 | 1. Start using like this: 107 | ```cs 108 | using DapperQueryBuilder; 109 | // ... 110 | 111 | cn = new SqlConnection(connectionString); 112 | 113 | // Build your query with interpolated parameters 114 | // which are automagically converted into safe SqlParameters 115 | var products = cn.QueryBuilder($@" 116 | SELECT ProductId, Name, ListPrice, Weight 117 | FROM Product 118 | WHERE ListPrice <= {maxPrice} 119 | AND Weight <= {maxWeight} 120 | AND Name LIKE {search} 121 | ORDER BY ProductId").Query(); 122 | ``` 123 | 124 | Or building dynamic conditions like this: 125 | ```cs 126 | using DapperQueryBuilder; 127 | // ... 128 | 129 | cn = new SqlConnection(connectionString); 130 | 131 | // Build initial query 132 | var q = cn.QueryBuilder($@" 133 | SELECT ProductId, Name, ListPrice, Weight 134 | FROM Product 135 | WHERE 1=1"); 136 | 137 | // and dynamically append extra filters 138 | q += $"AND ListPrice <= {maxPrice}"; 139 | q += $"AND Weight <= {maxWeight}"; 140 | q += $"AND Name LIKE {search}"; 141 | q += $"ORDER BY ProductId"; 142 | 143 | var products = q.Query(); 144 | ``` 145 | 146 | # Full Documentation and Features 147 | 148 | ## Static Query 149 | 150 | ```cs 151 | // Create a QueryBuilder with a static query. 152 | // QueryBuilder will automatically convert interpolated parameters to Dapper parameters (injection-safe) 153 | var q = cn.QueryBuilder($@" 154 | SELECT ProductId, Name, ListPrice, Weight FROM Product 155 | WHERE ListPrice <= {maxPrice} 156 | ORDER BY ProductId"); 157 | 158 | // Query() will automatically pass our query and injection-safe SqlParameters to Dapper 159 | var products = q.Query(); 160 | // all other Dapper extensions are also available: QueryAsync, QueryMultiple, ExecuteScalar, etc.. 161 | ``` 162 | 163 | So, basically you pass parameters as interpolated strings, but they are converted to safe SqlParameters. 164 | 165 | This is our mojo :-) 166 | 167 | ## Dynamic Query 168 | 169 | One of the top reasons for dynamically building SQL statements is to dynamically append new filters (`where` statements). 170 | 171 | ```cs 172 | // create a QueryBuilder with initial query 173 | var q = cn.QueryBuilder($"SELECT ProductId, Name, ListPrice, Weight FROM Product WHERE 1=1"); 174 | 175 | // Dynamically append whatever statements you need, and QueryBuilder will automatically 176 | // convert interpolated parameters to Dapper parameters (injection-safe) 177 | q += $"AND ListPrice <= {maxPrice}"; 178 | q += $"AND Weight <= {maxWeight}"; 179 | q += $"AND Name LIKE {search}"; 180 | q += $"ORDER BY ProductId"; 181 | 182 | var products = q.Query(); 183 | ``` 184 | 185 | ## Static Command 186 | 187 | ```cs 188 | var cmd = cn.CommandBuilder($"DELETE FROM Orders WHERE OrderId = {orderId};"); 189 | int deletedRows = cmd.Execute(); 190 | ``` 191 | 192 | ```cs 193 | cn.CommandBuilder($@" 194 | INSERT INTO Product (ProductName, ProductSubCategoryId) 195 | VALUES ({productName}, {ProductSubcategoryID}) 196 | ").Execute(); 197 | ``` 198 | 199 | 200 | ## Command with Multiple statements 201 | 202 | In a single roundtrip we can run multiple SQL commands: 203 | 204 | ```cs 205 | var cmd = cn.CommandBuilder(); 206 | cmd += $"DELETE FROM Orders WHERE OrderId = {orderId}; "; 207 | cmd += $"INSERT INTO Logs (Action, UserId, Description) VALUES ({action}, {orderId}, {description}); "; 208 | cmd.Execute(); 209 | ``` 210 | 211 | 212 | ## Dynamic Query with /\*\*where\*\*/ keyword 213 | 214 | If you don't like the idea of using `WHERE 1=1` (even though it [doesn't hurt performance](https://dba.stackexchange.com/a/33958/85815)), you can use the special keyword **/\*\*where\*\*/** that act as a placeholder to render dynamically-defined filters. 215 | 216 | `QueryBuilder` maintains an internal list of filters (property called `Filters`) which keeps track of all filters you've added using `.Where()` method. 217 | Then, when `QueryBuilder` invokes Dapper and sends the underlying query it will search for the keyword `/**where**/` in our query and if it exists it will replace it with the filters added (if any), combined using `AND` statements. 218 | 219 | 220 | Example: 221 | 222 | ```cs 223 | // We can write the query structure and use QueryBuilder to render the "where" filters (if any) 224 | var q = cn.QueryBuilder($@" 225 | SELECT ProductId, Name, ListPrice, Weight 226 | FROM Product 227 | /**where**/ 228 | ORDER BY ProductId 229 | "); 230 | 231 | // You just pass the parameters as if it was an interpolated string, 232 | // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe) 233 | q.Where($"ListPrice <= {maxPrice}"); 234 | q.Where($"Weight <= {maxWeight}"); 235 | q.Where($"Name LIKE {search}"); 236 | 237 | // Query() will automatically render your query and replace /**where**/ keyword (if any filter was added) 238 | var products = q.Query(); 239 | 240 | // In this case Dapper would get "WHERE ListPrice <= @p0 AND Weight <= @p1 AND Name LIKE @p2" and the associated values 241 | ``` 242 | 243 | When Dapper is invoked we replace the `/**where**/` by `WHERE AND AND ` (if any filter was added). 244 | 245 | ## Dynamic Query with /\*\*filters\*\*/ keyword 246 | 247 | **/\*\*filters\*\*/** is exactly like **/\*\*where\*\*/**, but it's used if we already have other fixed conditions before: 248 | ```cs 249 | var q = cn.QueryBuilder($@" 250 | SELECT ProductId, Name, ListPrice, Weight 251 | FROM Product 252 | WHERE Price>{minPrice} /**filters**/ 253 | ORDER BY ProductId 254 | "); 255 | ``` 256 | 257 | When Dapper is invoked we replace the `/**filters**/` by `AND AND ` (if any filter was added). 258 | 259 | 260 | ## Writing complex filters (combining AND/OR Filters) and using the Filters class 261 | 262 | As explained above, `QueryBuilder` internally contains an instance of `Filters` class, which basically contains a list of filters and a combining operator (default is AND but can be changed to OR). 263 | These filters are defined using `.Where()` and are rendered through the keywords `/**where**/` or `/**filters**/`. 264 | 265 | Each filter (inside a parent list of `Filters`) can be a simple condition (using interpolated strings) or it can recursively be another list of filters (`Filters` class), 266 | and this can be used to write complex combinations of AND/OR conditions (inner filters filters are grouped by enclosing parentheses): 267 | 268 | ```cs 269 | var q = cn.QueryBuilder($@" 270 | SELECT ProductId, Name, ListPrice, Weight 271 | FROM Product 272 | /**where**/ 273 | ORDER BY ProductId 274 | "); 275 | 276 | var priceFilters = new Filters(Filters.FiltersType.OR) 277 | { 278 | new Filter($"ListPrice >= {minPrice}"), 279 | new Filter($"ListPrice <= {maxPrice}") 280 | }; 281 | // Or we could add filters one by one like: priceFilters.Add($"Weight <= {maxWeight}"); 282 | 283 | q.Where("Status={status}"); 284 | // /**where**/ would be replaced by "Status=@p0" 285 | 286 | q.Where(priceFilters); 287 | // /**where**/ would be replaced as "Status=@p0 AND (ListPrice >= @p1 OR ListPrice <= @p2)". 288 | // Note that priceFilters is an inner Filter and it's enclosed with parentheses 289 | 290 | // It's also possible to change the combining operator of the outer query or of inner filters: 291 | // q.FiltersType = Filters.FiltersType.OR; 292 | // priceFilters.FiltersType = Filters.FiltersType.AND; 293 | // /**where**/ would be replaced as "Status=@p0 OR (ListPrice >= @p1 AND ListPrice >= @p2)". 294 | 295 | var products = q.Query(); 296 | ``` 297 | 298 | To sum, `Filters` class will render whatever conditions you define, conditions can be combined with `AND` or `OR`, and conditions can be defined as inner filters (will use parentheses). 299 | This is all vendor-agnostic (`AND`/`OR`/parentheses are all SQL ANSI) so it should work with any vendor. 300 | 301 | ## IN lists 302 | 303 | Dapper allows us to use IN lists magically. And it also works with our string interpolation: 304 | 305 | ```cs 306 | var q = cn.QueryBuilder($@" 307 | SELECT c.Name as Category, sc.Name as Subcategory, p.Name, p.ProductNumber 308 | FROM Product p 309 | INNER JOIN ProductSubcategory sc ON p.ProductSubcategoryID=sc.ProductSubcategoryID 310 | INNER JOIN ProductCategory c ON sc.ProductCategoryID=c.ProductCategoryID"); 311 | 312 | var categories = new string[] { "Components", "Clothing", "Acessories" }; 313 | q += $"WHERE c.Name IN {categories}"; 314 | ``` 315 | 316 | ## ValueTuple 317 | 318 | Dapper allows us to map rows to ValueTuples. And it also works with our string interpolation: 319 | 320 | ```cs 321 | // Sometimes we don't want to declare a class for a simple query 322 | var q = cn.QueryBuilder($@" 323 | SELECT Name, ListPrice, Weight 324 | FROM Product 325 | WHERE ProductId={productId}"); 326 | var productDetails = q.QuerySingle<(string Name, decimal ListPrice, decimal Weight)>(); 327 | ``` 328 | 329 | Warning: Dapper Tuple mapping is based on positions (it's not possible to map by names) 330 | 331 | 332 | ## Raw Modifier: Dynamic Column Names 333 | 334 | When we want to use regular string interpolation for building up our queries/commands but the interpolated values are not supposed to be converted into SQL parameters we can use the **raw modifier**. 335 | 336 | One popular example of the **raw modifier** is when we want to use **dynamic columns**: 337 | 338 | ```cs 339 | var query = connection.QueryBuilder($"SELECT * FROM Employee WHERE 1=1"); 340 | foreach(var filter in filters) 341 | query += $" AND {filter.ColumnName:raw} = {filter.Value}"; 342 | ``` 343 | 344 | Or: 345 | 346 | ```cs 347 | var query = connection.QueryBuilder($"SELECT * FROM Employee /**where**/"); 348 | foreach(var filter in filters) 349 | query.Where($"{filter.ColumnName:raw} = {filter.Value}"); 350 | ``` 351 | 352 | Whatever we pass as `:raw` should be either "trusted" or if it's untrusted (user-input) it should be sanitized correctly to avoid SQL-injection issues. (e.g. if `filter.ColumnName` comes from the UI we should validate it or sanitize it against SQL injection). 353 | 354 | 355 | ## Raw Modifier: Dynamic Table Names 356 | 357 | Another common use for **raw modifier** is when we're creating a global temporary table and want a unique (random) name: 358 | 359 | ```cs 360 | string uniqueId = Guid.NewGuid().ToString().Substring(0, 8); 361 | string name = "Rick"; 362 | 363 | cn.QueryBuilder($@" 364 | CREATE TABLE ##tmpTable{uniqueId:raw} 365 | ( 366 | Name nvarchar(200) 367 | ); 368 | INSERT INTO ##tmpTable{uniqueId:raw} (Name) VALUES ({name}); 369 | ").Execute(); 370 | ``` 371 | 372 | Let's emphasize again: strings that you interpolate using `:raw` modifier are not passed as parameters and therefore you should ensure validade it or sanitize it against SQL injection. 373 | 374 | 375 | ## Raw Modifier: nameof 376 | Another example of using the **raw modifier** is when we want to use **nameof expression** (which allows to "find references" for a column, "rename", etc): 377 | 378 | ```cs 379 | var q = cn.QueryBuilder($@" 380 | SELECT 381 | c.{nameof(Category.Name):raw} as Category, 382 | sc.{nameof(Subcategory.Name):raw} as Subcategory, 383 | p.{nameof(Product.Name):raw}, p.ProductNumber" 384 | FROM Product p 385 | INNER JOIN ProductSubcategory sc ON p.ProductSubcategoryID=sc.ProductSubcategoryID 386 | INNER JOIN ProductCategory c ON sc.ProductCategoryID=c.ProductCategoryID"); 387 | ``` 388 | 389 | ## Explicitly describing varchar/char data types (to avoid varchar performance issues) 390 | 391 | For Dapper (and consequently for us) strings are always are assumed to be unicode strings (nvarchar) by default. 392 | 393 | This causes a [known Dapper issue](https://jithilmt.medium.com/sql-server-hidden-load-evil-performance-issue-with-dapper-465a08f922f6): If the column datatype is varchar the query may not give the best performance and may even ignore existing indexed columns and do a full table scan. 394 | So for achieving best performance we may want to explicitly describe if our strings are unicode (nvarchar) or ansi (varchar), and also describe their exact sizes. 395 | 396 | Dapper's solution is to use the `DbString` class as a wrapper to describe the data type more explicitly, and QueryBuilder can also take this `DbString` in the interpolated values: 397 | 398 | ```cs 399 | string productName = "Mountain%"; 400 | 401 | // This is how we declare a varchar(50) in plain Dapper 402 | var productVarcharParm = new DbString { 403 | Value = productName, 404 | IsFixedLength = true, 405 | Length = 50, 406 | IsAnsi = true 407 | }; 408 | 409 | // DapperQueryBuilder understands Dapper DbString: 410 | var query = cn.QueryBuilder($@" 411 | SELECT * FROM Production.Product p 412 | WHERE Name LIKE {productVarcharParm}"); 413 | ``` 414 | 415 | **But we can also specify the datatype (using the well-established SQL syntax) after the value (`{value:datatype}`):** 416 | 417 | ```cs 418 | string productName = "Mountain%"; 419 | 420 | var query = cn.QueryBuilder($@" 421 | SELECT * FROM Production.Product p 422 | WHERE Name LIKE {productName:varchar(50)}"); 423 | ``` 424 | 425 | The library will parse the datatype specified after the colon, and it understands sql types like `varchar(size)`, `nvarchar(size)`, `char(size)`, `nchar(size)`, `varchar(MAX)`, `nvarchar(MAX)`. 426 | 427 | `nvarchar` and `nchar` are unicode strings, while `varchar` and `char` are ansi strings. 428 | `nvarchar` and `varchar` are variable-length strings, while `nchar` and `char` are fixed-length strings. 429 | 430 | Don't worry if your database does not use those exact types - we basically convert from those formats back into Dapper `DbString` class (with the appropriate hints `IsAnsi` and `IsFixedLength`), and Dapper will convert that to your database. 431 | 432 | 433 | ## Inner Queries 434 | 435 | It's possible to add sql-safe queries inside other queries (e.g. to use as subqueries) as long as you declare them as FormattableString. 436 | This makes it easier to break very complex queries into smaller methods/blocks, or reuse queries as subqueries. 437 | The parameters are fully preserved and safe: 438 | 439 | ```cs 440 | int orgId = 123; 441 | FormattableString innerQuery = $"SELECT Id, Name FROM SomeTable where OrganizationId={orgId}"; 442 | var q = cn.QueryBuilder($@" 443 | SELECT FROM ({innerQuery}) a 444 | JOIN AnotherTable b ON a.Id=b.Id 445 | WHERE b.OrganizationId={321}"); 446 | 447 | // q.Sql is like: 448 | // SELECT FROM (SELECT Id, Name FROM SomeTable where OrganizationId=@p0) a 449 | // JOIN AnotherTable b ON a.Id=b.Id 450 | // WHERE b.OrganizationId=@p1" 451 | ``` 452 | 453 | 454 | ## Fluent API (Chained-methods) 455 | 456 | 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. 457 | 458 | So, basically, instead of starting with a full query and just appending new filters (`.Where()`), the FluentQueryBuilder will build the whole query for you: 459 | 460 | ```cs 461 | var q = cn.FluentQueryBuilder() 462 | .Select($"ProductId") 463 | .Select($"Name") 464 | .Select($"ListPrice") 465 | .Select($"Weight") 466 | .From($"Product") 467 | .Where($"ListPrice <= {maxPrice}") 468 | .Where($"Weight <= {maxWeight}") 469 | .Where($"Name LIKE {search}") 470 | .OrderBy($"ProductId"); 471 | 472 | var products = q.Query(); 473 | ``` 474 | 475 | You would get this query: 476 | 477 | ```sql 478 | SELECT ProductId, Name, ListPrice, Weight 479 | FROM Product 480 | WHERE ListPrice <= @p0 AND Weight <= @p1 AND Name LIKE @p2 481 | ORDER BY ProductId 482 | ``` 483 | Or more elaborated: 484 | 485 | ```cs 486 | var q = cn.FluentQueryBuilder() 487 | .SelectDistinct($"ProductId, Name, ListPrice, Weight") 488 | .From("Product") 489 | .Where($"ListPrice <= {maxPrice}") 490 | .Where($"Weight <= {maxWeight}") 491 | .Where($"Name LIKE {search}") 492 | .OrderBy("ProductId"); 493 | ``` 494 | 495 | Building joins dynamically using Fluent API: 496 | 497 | ```cs 498 | var categories = new string[] { "Components", "Clothing", "Acessories" }; 499 | 500 | var q = cn.FluentQueryBuilder() 501 | .SelectDistinct($"c.Name as Category, sc.Name as Subcategory, p.Name, p.ProductNumber") 502 | .From($"Product p") 503 | .From($"INNER JOIN ProductSubcategory sc ON p.ProductSubcategoryID=sc.ProductSubcategoryID") 504 | .From($"INNER JOIN ProductCategory c ON sc.ProductCategoryID=c.ProductCategoryID") 505 | .Where($"c.Name IN {categories}"); 506 | ``` 507 | 508 | There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY). 509 | 510 | 511 | 512 | ## Using Type-Safe Filters without QueryBuilder 513 | 514 | If you want to use our type-safe dynamic filters but for some reason you don't want to use our QueryBuilder: 515 | 516 | ```cs 517 | Dapper.DynamicParameters parms = new Dapper.DynamicParameters(); 518 | 519 | var filters = new Filters(Filters.FiltersType.AND); 520 | filters.Add(new Filters() 521 | { 522 | new Filter($"ListPrice >= {minPrice}"), 523 | new Filter($"ListPrice <= {maxPrice}") 524 | }); 525 | filters.Add(new Filters(Filters.FiltersType.OR) 526 | { 527 | new Filter($"Weight <= {maxWeight}"), 528 | new Filter($"Name LIKE {search}") 529 | }); 530 | 531 | string where = filters.BuildFilters(parms); 532 | // "WHERE (ListPrice >= @p0 AND ListPrice <= @p1) AND (Weight <= @p2 OR Name LIKE @p3)" 533 | // parms contains @p0 as minPrice, @p1 as maxPrice, etc.. 534 | ``` 535 | 536 | ## Invoking Stored Procedures 537 | ```cs 538 | // This is basically Dapper, but with a FluentAPI where you can append parameters dynamically. 539 | var q = cn.CommandBuilder($"HumanResources.uspUpdateEmployeePersonalInfo") 540 | .AddParameter("ReturnValue", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue) 541 | .AddParameter("ErrorLogID", dbType: DbType.Int32, direction: ParameterDirection.Output) 542 | .AddParameter("BusinessEntityID", businessEntityID) 543 | .AddParameter("NationalIDNumber", nationalIDNumber) 544 | .AddParameter("BirthDate", birthDate) 545 | .AddParameter("MaritalStatus", maritalStatus) 546 | .AddParameter("Gender", gender); 547 | 548 | int affected = q.Execute(commandType: CommandType.StoredProcedure); 549 | int returnValue = q.Parameters.Get("ReturnValue"); 550 | ``` 551 | 552 | # Database Support 553 | 554 | QueryBuilder is database agnostic (like Dapper) - it should work with all ADO.NET providers (including Microsoft SQL Server, PostgreSQL, MySQL, SQLite, Firebird, SQL CE and Oracle), since it's basically a wrapper around the way parameters are passed to the database provider. 555 | 556 | DapperQueryBuilder doesn't generate SQL statements (except for simple clauses which should work in all databases like `WHERE`/`AND`/`OR` - if you're using `/**where**/` keyword). 557 | 558 | 559 | It was tested with **Microsoft SQL Server** and with **PostgreSQL** (using Npgsql driver), and works fine in both. 560 | 561 | ## Parameters prefix 562 | 563 | By default the parameters are generated using "at-parameters" format (the first parameter is named `@p0`, the next is `@p1`, etc), and that should work with most database providers (including PostgreSQL Npgsql). 564 | If your provider doesn't accept at-parameters (like Oracle) you can modify `DapperQueryBuilderOptions.DatabaseParameterSymbol`: 565 | 566 | ```cs 567 | // Default database-parameter-symbol is "@", which mean the underlying query will use @p0, @p1, etc.. 568 | // Some database vendors (like Oracle) expect ":" parameters instead of "@" parameters 569 | DapperQueryBuilderOptions.DatabaseParameterSymbol = ":"; 570 | 571 | OracleConnection cn = new OracleConnection("DATA SOURCE=server;PASSWORD=password;PERSIST SECURITY INFO=True;USER ID=user"); 572 | 573 | string search = "%Dinosaur%"; 574 | var cmd = cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}"); 575 | // Underlying SQL will be: SELECT * FROM film WHERE title like :p0 576 | ``` 577 | 578 | If for any reason you don't want parameters to be named `p0`, `p1`, etc, you can change the auto-naming prefix by setting `AutoGeneratedParameterName`: 579 | 580 | ```cs 581 | DapperQueryBuilderOptions.AutoGeneratedParameterName = "PARAM_"; 582 | 583 | // your parameters will be named @PARAM_0, @PARAM_1, etc.. 584 | ``` 585 | 586 | ## Some extra string magic we do: 587 | 588 | **Automatic spacing:** 589 | ```cs 590 | var query = cn.QueryBuilder($"SELECT * FROM Product WHERE 1=1"); 591 | query += $"AND Name LIKE {productName}"; 592 | query += $"AND ProductSubcategoryID = {subCategoryId}"; 593 | var products = query.Query(); 594 | ``` 595 | 596 | No need to worry about adding a space before or after a new clause. We'll handle that for you 597 | 598 | 599 | **Automatic strip of surrounding single-quotes**: 600 | 601 | If by mistake you add single quotes around interpolated arguments (as if it was dynamic SQL) we'll just strip it for you. 602 | 603 | ```cs 604 | cn.CommandBuilder($@" 605 | INSERT INTO Product (ProductName, ProductSubCategoryId) 606 | VALUES ('{productName}', '{ProductSubcategoryID}') 607 | ").Execute(); 608 | // Dapper will get "... VALUES (@p0, @p1) " (we'll remove the surrounding single quotes) 609 | ``` 610 | 611 | ```cs 612 | string productName = "%Computer%"; 613 | var products = cnQueryBuilder($"SELECT * FROM Product WHERE Name LIKE '{productName}'"); 614 | // Dapper will get "... WHERE Name LIKE @p0 " (we'll remove the surrounding single quotes) 615 | ``` 616 | 617 | **Automatic reuse of duplicated parameters**: 618 | 619 | If you use the same value twice in the query we'll just pass it once and reuse the existing parameter. 620 | 621 | ```cs 622 | string productName = "Computer"; 623 | var products = cnQueryBuilder($"SELECT * FROM Product WHERE Name = {productName} OR Category = {productName}"); 624 | // Dapper will get "... WHERE Name = @p0 OR Category = @p0 " (we'll send @p0 only once) 625 | ``` 626 | 627 | 628 | **Automatic trimming for multi-line queries**: 629 | ```cs 630 | var products = cn 631 | .Query($@" 632 | SELECT * FROM Product 633 | WHERE 634 | Name LIKE {productName} 635 | AND ProductSubcategoryID = {subCategoryId} 636 | ORDER BY ProductId"); 637 | ``` 638 | 639 | Since this is a multi-line interpolated string we'll automatically trim the first empty line and "dock to the left" (remove left padding). What Dapper receives does not have whitespace, making it easier for logging or debugging: 640 | ```sql 641 | SELECT * FROM Product 642 | WHERE 643 | Name LIKE @p0 644 | AND ProductSubcategoryID = @p1 645 | ORDER BY ProductId 646 | ``` 647 | 648 | 649 | 650 | 651 | 652 | # FAQ 653 | 654 | ## Why should we always use Parameterized Queries instead of Dynamic SQL? 655 | 656 | The whole purpose of Dapper is to safely map our objects to the database (and to map database records back to our objects). 657 | If you build SQL statements by concatenating parameters as strings it means that: 658 | 659 | - It would be more vulnerable to SQL injection. 660 | - You would have to manually sanitize your inputs [against SQL-injection attacks](https://stackoverflow.com/a/7505842) 661 | - You would have to manually convert null values 662 | - Your queries wouldn't benefit from cached execution plan 663 | - You would go crazy by not using Dapper like it was supposed to be used 664 | 665 | Building dynamic SQL (**which is a TERRIBLE idea**) would be like this: 666 | 667 | ```cs 668 | string sql = "SELECT * FROM Product WHERE Name LIKE " 669 | + "'" + productName.Replace("'", "''") + "'"; 670 | // now you pray that you've correctly sanitized inputs against sql-injection 671 | var products = cn.Query(sql); 672 | ``` 673 | 674 | With plain Dapper it's safer: 675 | ```cs 676 | string sql = "SELECT * FROM Product WHERE Name LIKE @productName"; 677 | var products = cn.Query(sql, new { productName }); 678 | ``` 679 | 680 | 681 | **But with Dapper Query Builder it's even easier**: 682 | ```cs 683 | var query = cn.QueryBuilder($"SELECT * FROM Product WHERE Name LIKE {productName}"); 684 | var products = query.Query(); 685 | ``` 686 | 687 | 688 | 689 | ## Why this library if we could just use interpolated strings directly with plain Dapper? 690 | 691 | Dapper does not take interpolated strings, and therefore it doesn't do any kind of manipulation magic on interpolated strings (which is exactly what we do). 692 | This means that if you pass an interpolated string to Dapper it will be converted as a plain string (**so it would run as dynamic SQL, not as parameterized SQL**), meaning it has **the same issues as dynamic sql** (see previous question). 693 | 694 | So it WOULD be possible (but ugly) to use Dapper with interpolated strings (as long as you sanitize the inputs): 695 | 696 | ```cs 697 | cn.Execute($@" 698 | INSERT INTO Product (ProductName, ProductSubCategoryId) 699 | VALUES ( 700 | '{productName.Replace("'", "''")}', 701 | {ProductSubcategoryID == null ? "NULL" : int.Parse(ProductSubcategoryID).ToString()} 702 | )"); 703 | // now you pray that you've correctly sanitized inputs against sql-injection 704 | ``` 705 | 706 | But with our library this is not only safer but also much simpler: 707 | 708 | ```cs 709 | cn.CommandBuilder($@" 710 | INSERT INTO Product (ProductName, ProductSubCategoryId) 711 | VALUES ({productName}, {ProductSubcategoryID}) 712 | ").Execute(); 713 | ``` 714 | 715 | In other words, passing interpolated strings to Dapper is dangerous because you may forget to sanitize the inputs. 716 | 717 | Our library makes the use of interpolated strings easier and safer because: 718 | - You don't have to sanitize the parameters (we rely on Dapper parameters) 719 | - You don't have to convert from nulls (we rely on Dapper parameters) 720 | - Our methods will never accept plain strings to avoid programmer mistakes 721 | - If you want to embed in the interpolated statement a regular string a do NOT want it to be converted to a parameter you need to explicitly describe it with the `:raw` modifier 722 | 723 | ## Why do I have to write everything using interpolated strings (`$`) 724 | 725 | The magic is that when you write an interpolated string our methods can identify the embedded parameters (interpolated values) and can correctly extract them and parameterize them. 726 | By enforcing that all methods only take `FormattableString` we can be confident that our methods will never receive an implicit conversion to string, which would defeat the whole purpose of the library and would make you vulnerable to SQL injection. 727 | The only way you can pass an unsafe string in your interpolation is if you explicitly add the **`:raw` modifier**, so it's easy to review all statements for vulnerabilities. 728 | As Alan Kay says, "Simple things should be simple and complex things should be possible" - so interpolating regular sql parameters is very simple, while interpolating plain strings is still possible. 729 | 730 | ## Is building queries with string interpolation really safe? 731 | 732 | In our library String Interpolation is just an abstraction used for hiding the complexity of manually creating SqlParameters. 733 | This library is as safe as possible because it never accepts plain strings, so there's no risk of accidentally converting an interpolated string into a vulnerable string. But obviously there are a few possible scenarios where mistakes could happen. 734 | 735 | **First possible mistake - using raw modifier for things that should be parameters:** 736 | 737 | ```cs 738 | using DapperQueryBuilder; 739 | 740 | // If you don't understand what raw is for, DON'T USE IT - code below is unsafe! 741 | var products = cn.QueryBuilder($@" 742 | SELECT * FROM Product WHERE ProductId={productId:raw}" 743 | ).Query(); 744 | ``` 745 | 746 | **Second possible mistake - passing interpolated strings to Dapper instead of DapperQueryBuilder:** 747 | 748 | ```cs 749 | using Dapper; 750 | 751 | // UNSAFE CODE. Dapper will get an unsafe (not parameterized) query. 752 | var products = cn.Query($@" 753 | SELECT * FROM Product WHERE ProductId={productId}" 754 | ); 755 | 756 | // To avoid this type of mistake you can just avoid Dapper namespace 757 | // and just use "using DapperQueryBuilder;" 758 | ``` 759 | 760 | **Third possible mistake - Create a "fake" FormattableString by passing an unsafe plain string to FormattableStringFactory:** 761 | 762 | ```cs 763 | using DapperQueryBuilder; 764 | using System.Runtime.CompilerServices; // needs System.Runtime.dll 765 | 766 | // Explicitly create an interpolated string in a totally incorrect way 767 | var products = cn.QueryBuilder(FormattableStringFactory.Create($@" 768 | SELECT * FROM Product WHERE ProductId={productId}") 769 | ).Query(); 770 | 771 | // FormattableStringFactory.Create above is used totally incorrect. 772 | // Basically the interpolated string will be converted into an unsafe string 773 | // and then it's converted back into a fake interpolated string. 774 | 775 | ``` 776 | 777 | 778 | ## How can I use Dapper async extensions? 779 | 780 | This documentation is mostly using sync methods, but we have [facades](/src/DapperQueryBuilder/ICompleteCommandExtensions.cs) for **all** Dapper extensions, including `ExecuteAsync()`, `QueryAsync`, etc. 781 | 782 | ## How can I use Dapper Multi-Mapping? 783 | 784 | We do not have facades for invoking Dapper Multi-mapper extensions directly, but you can combine QueryBuilder with Multi-mapper extensions by explicitly passing CommandBuillder `.Sql` and `.Parameters`: 785 | 786 | ```cs 787 | // DapperQueryBuilder CommandBuilder 788 | var orderAndDetailsQuery = cn 789 | .QueryBuilder($@" 790 | SELECT * FROM Orders AS A 791 | INNER JOIN OrderDetails AS B ON A.OrderID = B.OrderID 792 | /**where**/ 793 | ORDER BY A.OrderId, B.OrderDetailId"); 794 | // Dynamic Filters 795 | orderAndDetailsQuery.Where($"[ListPrice] <= {maxPrice}"); 796 | orderAndDetailsQuery.Where($"[Weight] <= {maxWeight}"); 797 | orderAndDetailsQuery.Where($"[Name] LIKE {search}"); 798 | 799 | // Dapper Multi-mapping extensions 800 | var orderAndDetails = cn.Query( 801 | /* orderAndDetailsQuery.Sql contains [ListPrice] <= @p0 AND [Weight] <= @p1 etc... */ 802 | sql: orderAndDetailsQuery.Sql, 803 | map: (order, orderDetail) => 804 | { 805 | // whatever.. 806 | }, 807 | /* orderAndDetailsQuery.Parameters contains @p0, @p1 etc... */ 808 | param: orderAndDetailsQuery.Parameters, 809 | splitOn: "OrderDetailID") 810 | .Distinct() 811 | .ToList(); 812 | ``` 813 | 814 | To sum, instead of using DapperQueryBuilder `.Query` extensions (which invoke Dapper `IDbConnection.Query`) you just invoke Dapper multimapper `.Query` directly, and use DapperQueryBuilder only for dynamically building your filters. 815 | 816 | ## How does DapperQueryBuilder compare to plain Dapper? 817 | 818 | **Building dynamic filters in plain Dapper is a little cumbersome / ugly:** 819 | ```cs 820 | var parms = new DynamicParameters(); 821 | List filters = new List(); 822 | 823 | filters.Add("Name LIKE @productName"); 824 | parms.Add("productName", productName); 825 | filters.Add("CategoryId = @categoryId"); 826 | parms.Add("categoryId", categoryId); 827 | 828 | string where = (filters.Any() ? " WHERE " + string.Join(" AND ", filters) : ""); 829 | 830 | var products = cn.Query($@" 831 | SELECT * FROM Product 832 | {where} 833 | ORDER BY ProductId", parms); 834 | ``` 835 | 836 | **With DapperQueryBuilder it's much easier to write queries with dynamic filters:** 837 | ```cs 838 | var query = cn.QueryBuilder($@" 839 | SELECT * FROM Product 840 | /**where**/ 841 | ORDER BY ProductId"); 842 | 843 | query.Where($"Name LIKE {productName}"); 844 | query.Where($"CategoryId = {categoryId}"); 845 | 846 | // If any filter was added, Query() will automatically replace the /**where**/ keyword 847 | var products = query.Query(); 848 | ``` 849 | 850 | or without `/**where**/`: 851 | ```cs 852 | var query = cn.QueryBuilder($"SELECT * FROM Product WHERE 1=1"); 853 | query += $"AND Name LIKE {productName}"; 854 | query += $"AND CategoryId = {categoryId}"; 855 | query += $"ORDER BY ProductId"; 856 | var products = query.Query(); 857 | ``` 858 | 859 | 860 | ## How does DapperQueryBuilder compare to [Dapper.SqlBuilder](https://github.com/DapperLib/Dapper/tree/main/Dapper.SqlBuilder)? 861 | 862 | Dapper.SqlBuilder combines the filters using `/**where**/` keyword (like we do) but requires some auxiliar classes, and filters have to be defined using Dapper syntax (no string interpolation): 863 | 864 | ```cs 865 | // SqlBuilder and Template are helper classes 866 | var builder = new SqlBuilder(); 867 | 868 | // We also use this /**where**/ syntax 869 | var template = builder.AddTemplate(@" 870 | SELECT * FROM Product 871 | /**where**/ 872 | ORDER BY ProductId"); 873 | 874 | // Define the filters using regular Dapper syntax: 875 | builder.Where("Name LIKE @productName", new { productName }); 876 | builder.Where("CategoryId = @categoryId", new { categoryId }); 877 | 878 | // Use template to explicitly pass the rendered SQL and parameters to Dapper: 879 | var products = cn.Query(template.RawSql, template.Parameters); 880 | ``` 881 | 882 | 883 | ## Why don't you have Typed Filters using Lambda Expressions? 884 | 885 | We believe that SQL syntax is powerful, comprehensive and vendor-specific. Dapper allows us to use the full SQL syntax (of our database vendor), and so does DapperQueryBuilder. 886 | That's why we decided to focus on our magic (converting interpolated strings into SQL parameters), while keeping Dapper simplicity (you write your own filters). 887 | In other words, we won't try to reinvent SQL syntax or create a limited abstraction over SQL language. 888 | 889 | 890 | ## How to Collaborate? 891 | 892 | Please submit a pull-request or if you want to make a sugestion you can [create an issue](https://github.com/Drizin/DapperQueryBuilder/issues) or [contact me](https://rdrizin.com/pages/Contact/). 893 | 894 | ## Stargazers over time 895 | 896 | [![Star History Chart](https://api.star-history.com/svg?repos=Drizin/DapperQueryBuilder&type=Date)](https://star-history.com/#Drizin/DapperQueryBuilder&Date) 897 | 898 | 899 | ## License 900 | MIT License 901 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 2 | [*.cs] 3 | 4 | 5 | #Core editorconfig formatting - indentation 6 | 7 | #use soft tabs (spaces) for indentation 8 | indent_style = space 9 | 10 | #Formatting - new line options 11 | 12 | #place catch statements on a new line 13 | csharp_new_line_before_catch = true 14 | #place else statements on a new line 15 | csharp_new_line_before_else = true 16 | #require members of object intializers to be on separate lines 17 | csharp_new_line_before_members_in_object_initializers = true 18 | #require braces to be on a new line for object_collection_array_initializers, methods, types, control_blocks, properties, and accessors (also known as "Allman" style) 19 | csharp_new_line_before_open_brace = object_collection_array_initializers, methods, types, control_blocks, properties, accessors 20 | 21 | #Formatting - organize using options 22 | 23 | #do not place System.* using directives before other using directives 24 | dotnet_sort_system_directives_first = false 25 | 26 | #Formatting - spacing options 27 | 28 | #require NO space between a cast and the value 29 | csharp_space_after_cast = false 30 | #require a space before the colon for bases or interfaces in a type declaration 31 | csharp_space_after_colon_in_inheritance_clause = true 32 | #require a space after a keyword in a control flow statement such as a for loop 33 | csharp_space_after_keywords_in_control_flow_statements = true 34 | #require a space before the colon for bases or interfaces in a type declaration 35 | csharp_space_before_colon_in_inheritance_clause = true 36 | #remove space within empty argument list parentheses 37 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 38 | #remove space between method call name and opening parenthesis 39 | csharp_space_between_method_call_name_and_opening_parenthesis = false 40 | #do not place space characters after the opening parenthesis and before the closing parenthesis of a method call 41 | csharp_space_between_method_call_parameter_list_parentheses = false 42 | #remove space within empty parameter list parentheses for a method declaration 43 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 44 | #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. 45 | csharp_space_between_method_declaration_parameter_list_parentheses = false 46 | 47 | #Formatting - wrapping options 48 | 49 | #leave code block on single line 50 | csharp_preserve_single_line_blocks = true 51 | 52 | #Style - Code block preferences 53 | 54 | #prefer no curly braces if allowed 55 | csharp_prefer_braces = false:suggestion 56 | 57 | #Style - expression bodied member options 58 | 59 | #prefer expression-bodied members for accessors 60 | csharp_style_expression_bodied_accessors = true:suggestion 61 | #prefer block bodies for constructors 62 | csharp_style_expression_bodied_constructors = false:suggestion 63 | #prefer block bodies for methods 64 | csharp_style_expression_bodied_methods = false:suggestion 65 | #prefer expression-bodied members for properties 66 | csharp_style_expression_bodied_properties = true:suggestion 67 | 68 | #Style - expression level options 69 | 70 | #prefer out variables to be declared before the method call 71 | csharp_style_inlined_variable_declaration = false:suggestion 72 | #prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them 73 | dotnet_style_predefined_type_for_member_access = true:suggestion 74 | 75 | #Style - Expression-level preferences 76 | 77 | #prefer objects to be initialized using object initializers when possible 78 | dotnet_style_object_initializer = true:suggestion 79 | 80 | #Style - implicit and explicit types 81 | 82 | #prefer explicit type over var in all cases, unless overridden by another code style rule 83 | csharp_style_var_elsewhere = false:suggestion 84 | #prefer explicit type over var to declare variables with built-in system types such as int 85 | csharp_style_var_for_built_in_types = false:suggestion 86 | #prefer var when the type is already mentioned on the right-hand side of a declaration expression 87 | csharp_style_var_when_type_is_apparent = true:suggestion 88 | 89 | #Style - language keyword and framework type options 90 | 91 | #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them 92 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 93 | 94 | #Style - Miscellaneous preferences 95 | 96 | #prefer anonymous functions over local functions 97 | csharp_style_pattern_local_over_anonymous_function = false:suggestion 98 | 99 | #Style - modifier options 100 | 101 | #prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. 102 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 103 | 104 | #Style - Modifier preferences 105 | 106 | #when this rule is set to a list of modifiers, prefer the specified ordering. 107 | csharp_preferred_modifier_order = public,private,protected,internal,static,readonly,virtual:suggestion 108 | 109 | #Style - Pattern matching 110 | 111 | #prefer pattern matching instead of is expression with type casts 112 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 113 | 114 | #Style - qualification options 115 | 116 | #prefer fields not to be prefaced with this. or Me. in Visual Basic 117 | dotnet_style_qualification_for_field = false:suggestion 118 | #prefer methods not to be prefaced with this. or Me. in Visual Basic 119 | dotnet_style_qualification_for_method = false:suggestion 120 | #prefer properties not to be prefaced with this. or Me. in Visual Basic 121 | dotnet_style_qualification_for_property = false:suggestion 122 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net462;net472;net5.0;net6.0;net7.0 5 | Rick Drizin 6 | MIT 7 | https://github.com/Drizin/DapperQueryBuilder/ 8 | Dapper Query Builder using Fluent API and String Interpolation 9 | Rick Drizin 10 | Rick Drizin 11 | 2.0.0 12 | false 13 | DapperQueryBuilder (Strong Named) 14 | DapperQueryBuilder.StrongName 15 | DapperQueryBuilder.StrongName.xml 16 | dapper;query-builder;query builder;dapperquerybuilder;dapper-query-builder;dapper-interpolation;dapper-interpolated-string 17 | true 18 | true 19 | 20 | NuGetReadMe.md 21 | DapperQueryBuilder.StrongName 22 | enable 23 | 8.0 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | True 42 | ..\debug.snk 43 | 44 | 45 | 46 | 47 | True 48 | ..\release.snk 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.StrongName/DapperQueryBuilder.StrongName.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DapperQueryBuilder.StrongName 5 | DapperQueryBuilder (Strong Named) 6 | Rick Drizin 7 | Rick Drizin 8 | MIT 9 | https://github.com/Drizin/DapperQueryBuilder 10 | false 11 | DapperQueryBuilder: Dapper Query Builder using String Interpolation and Fluent API 12 | Copyright Rick Drizin 2020 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/CommandBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Data.SqlClient; 8 | using System.Linq; 9 | 10 | namespace DapperQueryBuilder.Tests 11 | { 12 | [TestFixture(true)] 13 | [TestFixture(false)] 14 | 15 | public class CommandBuilderTests 16 | { 17 | IDbConnection cn; 18 | 19 | public CommandBuilderTests() { } // nunit requires parameterless constructor 20 | public CommandBuilderTests(bool reuseIdenticalParameters) 21 | { 22 | InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.ReuseIdenticalParameters = reuseIdenticalParameters; 23 | } 24 | 25 | #region Setup 26 | [SetUp] 27 | public void Setup() 28 | { 29 | cn = new SqlConnection(TestHelper.GetMSSQLConnectionString()); 30 | } 31 | #endregion 32 | 33 | int businessEntityID = 1; 34 | string nationalIDNumber = "295847284"; 35 | DateTime birthDate = new DateTime(1969, 01, 29); 36 | string maritalStatus = "S"; // single 37 | string gender = "M"; 38 | 39 | public class Product 40 | { 41 | public int ProductId { get; set; } 42 | public string Name { get; set; } 43 | } 44 | 45 | [Test] 46 | public void TestBareCommand() 47 | { 48 | string productName = "%mountain%"; 49 | int subCategoryId = 12; 50 | 51 | var query = cn 52 | .QueryBuilder($$""" 53 | SELECT * FROM [Production].[Product] 54 | WHERE 55 | [Name] LIKE {{productName}} 56 | AND [ProductSubcategoryID] = {{subCategoryId}} 57 | ORDER BY [ProductId] 58 | """); 59 | 60 | Assert.AreEqual(@" 61 | SELECT * FROM [Production].[Product] 62 | WHERE 63 | [Name] LIKE @p0 64 | AND [ProductSubcategoryID] = @p1 65 | ORDER BY [ProductId]".TrimStart(), query.AsSql().Sql); 66 | 67 | var products = query.Query(); 68 | } 69 | 70 | 71 | [Test] 72 | public void TestNameof() 73 | { 74 | string productName = "%mountain%"; 75 | int subCategoryId = 12; 76 | 77 | var query = cn 78 | .QueryBuilder($$""" 79 | SELECT * FROM [Production].[Product] 80 | WHERE 81 | [{{nameof(Product.Name):raw}}] LIKE {{productName}} 82 | AND [ProductSubcategoryID] = {{subCategoryId}} 83 | ORDER BY [ProductId] 84 | """); 85 | 86 | Assert.AreEqual(@" 87 | SELECT * FROM [Production].[Product] 88 | WHERE 89 | [Name] LIKE @p0 90 | AND [ProductSubcategoryID] = @p1 91 | ORDER BY [ProductId]".TrimStart(), query.AsSql().Sql); 92 | 93 | var products = query.Query(); 94 | } 95 | 96 | [Test] 97 | public void TestAppends() 98 | { 99 | string productName = "%mountain%"; 100 | int subCategoryId = 12; 101 | 102 | var query = cn 103 | .SqlBuilder($@"SELECT * FROM [Production].[Product]") 104 | .AppendLine($"WHERE") 105 | .AppendLine($"[Name] LIKE {productName}") 106 | .AppendLine($"AND [ProductSubcategoryID] = {subCategoryId}") 107 | .AppendLine($"ORDER BY [ProductId]"); 108 | Assert.AreEqual( 109 | @"SELECT * FROM [Production].[Product] 110 | WHERE 111 | [Name] LIKE @p0 112 | AND [ProductSubcategoryID] = @p1 113 | ORDER BY [ProductId]", query.AsSql().Sql); 114 | 115 | var products = query.Query(); 116 | } 117 | 118 | [Test] 119 | public void TestAutoSpacing() 120 | { 121 | string productName = "%mountain%"; 122 | int subCategoryId = 12; 123 | 124 | var query = cn 125 | .SqlBuilder($@"SELECT * FROM [Production].[Product]") 126 | .Append($"WHERE") 127 | .Append($"[Name] LIKE {productName}") 128 | .Append($"AND [ProductSubcategoryID] = {subCategoryId}") 129 | .Append($"ORDER BY [ProductId]"); 130 | 131 | Assert.AreEqual(@"SELECT * FROM [Production].[Product] WHERE [Name] LIKE @p0 AND [ProductSubcategoryID] = @p1 ORDER BY [ProductId]", query.AsSql().Sql); 132 | 133 | var products = query.Query(); 134 | } 135 | 136 | [Test] 137 | public void TestStoredProcedure() 138 | { 139 | var q = cn.SqlBuilder($"[HumanResources].[uspUpdateEmployeePersonalInfo]") 140 | .AddParameter("BusinessEntityID", businessEntityID) 141 | .AddParameter("NationalIDNumber", nationalIDNumber) 142 | .AddParameter("BirthDate", birthDate) 143 | .AddParameter("MaritalStatus", maritalStatus) 144 | .AddParameter("Gender", gender); 145 | int affected = q.Execute(commandType: CommandType.StoredProcedure); 146 | } 147 | 148 | [Test] 149 | public void TestStoredProcedureExec() 150 | { 151 | var q = cn.SqlBuilder($@" 152 | DECLARE @ret INT; 153 | EXEC @RET = [HumanResources].[uspUpdateEmployeePersonalInfo] 154 | @BusinessEntityID={businessEntityID} 155 | ,@NationalIDNumber={nationalIDNumber} 156 | ,@BirthDate={birthDate} 157 | ,@MaritalStatus={maritalStatus} 158 | ,@Gender={gender}; 159 | SELECT @RET; 160 | "); 161 | 162 | int affected = q.Execute(); 163 | } 164 | 165 | public class MyPoco { public int MyValue { get; set; } } 166 | 167 | [Test] 168 | public void TestStoredProcedureOutput() 169 | { 170 | MyPoco poco = new MyPoco(); 171 | 172 | var cmd = cn.SqlBuilder($"[dbo].[sp_TestOutput]") 173 | .AddParameter("Input1", dbType: DbType.Int32); 174 | //.AddParameter("Output1", dbType: DbType.Int32, direction: ParameterDirection.Output); 175 | //var getter = ParameterInfos.GetSetter((MyPoco p) => p.MyValue); 176 | var outputParm = new DbTypeParameterInfo("Output1", size: 4); 177 | outputParm.ConfigureOutputParameter(poco, p => p.MyValue, SqlParameterInfo.OutputParameterDirection.Output); 178 | cmd.AddParameter(outputParm); //TODO: AddOutputParameter? move ConfigureOutputParameter inside it. // previously this was cmd.Parameters.Add, but not Parameters is get-only 179 | int affected = cmd.Execute(commandType: CommandType.StoredProcedure); 180 | 181 | string outputValue = cmd.Parameters.Get("Output1"); // why is this being returned as string? just because I didn't provide type above? 182 | Assert.AreEqual(outputValue, "2"); 183 | 184 | Assert.AreEqual(poco.MyValue, 2); 185 | } 186 | 187 | [Test] 188 | public void TestCRUD() 189 | { 190 | string id = "123"; 191 | int affected = cn.SqlBuilder($@"UPDATE [HumanResources].[Employee] SET") 192 | .Append($"NationalIDNumber={id}") 193 | .Append($"WHERE BusinessEntityID={businessEntityID}") 194 | .Execute(); 195 | } 196 | 197 | /// 198 | /// Quotes around interpolated arguments should be automtaically detected and ignored 199 | /// 200 | [Test] 201 | public void TestQuotes1() 202 | { 203 | string search = "%mountain%"; 204 | string expected = "SELECT * FROM [Production].[Product] WHERE [Name] LIKE @p0"; 205 | var cmd = cn.SqlBuilder($@"SELECT * FROM [Production].[Product] WHERE [Name] LIKE {search}"); 206 | var cmd2 = cn.SqlBuilder($@"SELECT * FROM [Production].[Product] WHERE [Name] LIKE '{search}'"); 207 | 208 | Assert.AreEqual(expected, cmd.AsSql().Sql); 209 | Assert.AreEqual(expected, cmd2.AsSql().Sql); 210 | 211 | var products = cmd.Query(); 212 | Assert.That(products.Any()); 213 | var products2 = cmd2.Query(); 214 | Assert.That(products2.Any()); 215 | } 216 | 217 | 218 | /// 219 | /// Quotes around interpolated arguments should be automtaically detected and ignored 220 | /// 221 | [Test] 222 | public void TestQuotes2() 223 | { 224 | string productNumber = "AR-5381"; 225 | string expected = "SELECT * FROM [Production].[Product] WHERE [ProductNumber]=@p0"; 226 | var cmd = cn.SqlBuilder($@"SELECT * FROM [Production].[Product] WHERE [ProductNumber]='{productNumber}'"); 227 | var cmd2 = cn.SqlBuilder($@"SELECT * FROM [Production].[Product] WHERE [ProductNumber]={productNumber}"); 228 | 229 | Assert.AreEqual(expected, cmd.AsSql().Sql); 230 | Assert.AreEqual(expected, cmd2.AsSql().Sql); 231 | 232 | var products = cmd.Query(); 233 | Assert.That(products.Any()); 234 | var products2 = cmd2.Query(); 235 | Assert.That(products2.Any()); 236 | } 237 | 238 | 239 | /// 240 | /// Quotes around interpolated arguments should be automtaically detected and ignored 241 | /// 242 | [Test] 243 | public void TestQuotes3() 244 | { 245 | string productNumber = "AR-5381"; 246 | string expected = "SELECT * FROM [Production].[Product] WHERE @p0<=[ProductNumber]"; 247 | var cmd = cn.SqlBuilder($@"SELECT * FROM [Production].[Product] WHERE '{productNumber}'<=[ProductNumber]"); 248 | var cmd2 = cn.SqlBuilder($@"SELECT * FROM [Production].[Product] WHERE {productNumber}<=[ProductNumber]"); 249 | 250 | Assert.AreEqual(expected, cmd.AsSql().Sql); 251 | Assert.AreEqual(expected, cmd2.AsSql().Sql); 252 | 253 | var products = cmd.Query(); 254 | Assert.That(products.Any()); 255 | var products2 = cmd2.Query(); 256 | Assert.That(products2.Any()); 257 | } 258 | 259 | 260 | /// 261 | /// Quotes around interpolated arguments should not be ignored if it's raw string 262 | /// 263 | [Test] 264 | public void TestQuotes4() 265 | { 266 | string literal = "Hello"; 267 | string search = "%mountain%"; 268 | 269 | string expected = "SELECT 'Hello' FROM [Production].[Product] WHERE [Name] LIKE @p0"; 270 | var cmd = cn.SqlBuilder($@"SELECT '{literal:raw}' FROM [Production].[Product] WHERE [Name] LIKE {search}"); // quotes will be preserved 271 | 272 | string expected2 = "SELECT @p0 FROM [Production].[Product] WHERE [Name] LIKE @p1"; 273 | var cmd2 = cn.SqlBuilder($@"SELECT '{literal}' FROM [Production].[Product] WHERE [Name] LIKE {search}"); // quotes will be striped 274 | 275 | Assert.AreEqual(expected, cmd.AsSql().Sql); 276 | Assert.AreEqual(expected2, cmd2.AsSql().Sql); 277 | 278 | var products = cmd.Query(); 279 | Assert.That(products.Any()); 280 | 281 | var products2 = cmd2.Query(); 282 | Assert.That(products2.Any()); 283 | } 284 | 285 | 286 | [Test] 287 | public void TestAutospacing() 288 | { 289 | string search = "%mountain%"; 290 | var cmd = cn.SqlBuilder($@"SELECT * FROM [Production].[Product]"); 291 | cmd.Append($"WHERE [Name] LIKE {search}"); 292 | cmd.Append($"AND 1=1"); 293 | Assert.AreEqual("SELECT * FROM [Production].[Product] WHERE [Name] LIKE @p0 AND 1=1", cmd.AsSql().Sql); 294 | } 295 | 296 | [Test] 297 | public void TestOperatorOverload() 298 | { 299 | string search = "%mountain%"; 300 | var cmd = cn.SqlBuilder() 301 | + $@"SELECT * FROM [Production].[Product]" 302 | + $"WHERE [Name] LIKE {search}"; 303 | cmd += $"AND 1=1"; 304 | Assert.AreEqual("SELECT * FROM [Production].[Product] WHERE [Name] LIKE @p0 AND 1=1", cmd.AsSql().Sql); 305 | } 306 | 307 | [Test] 308 | public void TestAutospacing2() 309 | { 310 | string search = "%mountain%"; 311 | var cmd = cn.SqlBuilder($$""" 312 | SELECT * FROM [Production].[Product] 313 | WHERE [Name] LIKE {{search}} 314 | AND 1=2 315 | """); 316 | Assert.AreEqual( 317 | "SELECT * FROM [Production].[Product]" + Environment.NewLine + 318 | "WHERE [Name] LIKE @p0" + Environment.NewLine + 319 | "AND 1=2", cmd.AsSql().Sql); 320 | } 321 | 322 | [Test] 323 | public void TestAutospacing3() 324 | { 325 | string productNumber = "EC-M092"; 326 | int productId = 328; 327 | var cmd = cn.SqlBuilder($$""" 328 | UPDATE [Production].[Product] 329 | SET [ProductNumber]={{productNumber}} 330 | WHERE [ProductId]={{productId}} 331 | """); 332 | 333 | string expected = 334 | "UPDATE [Production].[Product]" + Environment.NewLine + 335 | "SET [ProductNumber]=@p0" + Environment.NewLine + 336 | "WHERE [ProductId]=@p1"; 337 | 338 | Assert.AreEqual(expected, cmd.AsSql().Sql); 339 | } 340 | 341 | [Test] 342 | public void TestAutospacing4() 343 | { 344 | string productNumber = "EC-M092"; 345 | int productId = 328; 346 | 347 | var cmd = cn.SqlBuilder($@"UPDATE [Production].[Product]") 348 | .Append($"SET [ProductNumber]={productNumber}") 349 | .Append($"WHERE [ProductId]={productId}"); 350 | 351 | string expected = "UPDATE [Production].[Product] SET [ProductNumber]=@p0 WHERE [ProductId]=@p1"; 352 | 353 | Assert.AreEqual(expected, cmd.AsSql().Sql); 354 | } 355 | 356 | 357 | [Test] 358 | public void TestQueryBuilderWithAppends() 359 | { 360 | string productName = "%mountain%"; 361 | int subCategoryId = 12; 362 | 363 | var query = cn 364 | .QueryBuilder($@"SELECT * FROM [Production].[Product] WHERE [Name] LIKE {productName}"); 365 | query.AppendLine($"AND [ProductSubcategoryID] = {subCategoryId} ORDER BY {2}"); 366 | Assert.AreEqual(@"SELECT * FROM [Production].[Product] WHERE [Name] LIKE @p0 367 | AND [ProductSubcategoryID] = @p1 ORDER BY @p2", query.AsSql().Sql); 368 | 369 | //var products = query.Query(); 370 | } 371 | 372 | [Test] 373 | public void TestQueryBuilderWithAppends2() 374 | { 375 | string productName = "%mountain%"; 376 | int subCategoryId = 12; 377 | 378 | var query = cn 379 | .QueryBuilder($@"SELECT * FROM [Production].[Product] WHERE [Name] LIKE {productName}"); 380 | query.AppendLine($"AND [ProductSubcategoryID]={subCategoryId} ORDER BY {2}"); 381 | Assert.AreEqual(@"SELECT * FROM [Production].[Product] WHERE [Name] LIKE @p0 382 | AND [ProductSubcategoryID]=@p1 ORDER BY @p2", query.AsSql().Sql); 383 | 384 | //var products = query.Query(); 385 | } 386 | 387 | 388 | [Test] 389 | public void TestRepeatedParameters() 390 | { 391 | string username = "rdrizin"; 392 | int subCategoryId = 12; 393 | int? categoryId = null; 394 | 395 | var query = cn.QueryBuilder($@"SELECT * FROM [table1] WHERE ([Name]={username} or [Author]={username}"); 396 | query.Append($"or [Creator]={username})"); 397 | query.Append($"AND ([ProductSubcategoryID]={subCategoryId}"); 398 | query.Append($"OR [ProductSubcategoryID]={categoryId}"); 399 | query.Append($"OR [ProductCategoryID]={subCategoryId}"); 400 | query.Append($"OR [ProductCategoryID]={categoryId})"); 401 | 402 | if (InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.ReuseIdenticalParameters) 403 | { 404 | Assert.AreEqual(@"SELECT * FROM [table1] WHERE ([Name]=@p0 or [Author]=@p0" 405 | + " or [Creator]=@p0)" 406 | + " AND ([ProductSubcategoryID]=@p1" 407 | + " OR [ProductSubcategoryID]=@p2" 408 | + " OR [ProductCategoryID]=@p1" 409 | + " OR [ProductCategoryID]=@p2)" 410 | , query.AsSql().Sql); 411 | Assert.AreEqual(query.Parameters.Get("p1"), 12); 412 | Assert.AreEqual(query.Parameters.Get("p2"), null); 413 | } 414 | else 415 | { 416 | Assert.AreEqual(@"SELECT * FROM [table1] WHERE ([Name]=@p0 or [Author]=@p1" 417 | + " or [Creator]=@p2)" 418 | + " AND ([ProductSubcategoryID]=@p3" 419 | + " OR [ProductSubcategoryID]=@p4" 420 | + " OR [ProductCategoryID]=@p5" 421 | + " OR [ProductCategoryID]=@p6)" 422 | , query.AsSql().Sql); 423 | Assert.AreEqual(query.Parameters.Get("p3"), 12); 424 | Assert.AreEqual(query.Parameters.Get("p5"), 12); 425 | Assert.AreEqual(query.Parameters.Get("p4"), null); 426 | Assert.AreEqual(query.Parameters.Get("p6"), null); 427 | } 428 | } 429 | 430 | 431 | [Test] 432 | public void TestRepeatedParameters2() 433 | { 434 | if (!InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.ReuseIdenticalParameters) 435 | return; 436 | 437 | int? fileId = null; 438 | string backupFileName = null; 439 | var folderKey = 93572; 440 | var secureFileName = "upload.txt"; 441 | var uploadDate = DateTime.Now; 442 | string description = null; 443 | int? size = null; 444 | var cacheId = new Guid("{95b94695-b7ec-4e3a-9260-f815cceb5ff1}"); 445 | var version = new Guid("{95b94695-b7ec-4e3a-9260-f815cceb5ff1}"); 446 | var user = "terry.aney"; 447 | var contentType = "application/pdf"; 448 | var folder = "btr.aney.terry"; 449 | 450 | var query = cn.QueryBuilder($@"DECLARE @fKey int; SET @fKey = {fileId}; 451 | DECLARE @backupFileName VARCHAR(1); SET @backupFileName = {backupFileName}; 452 | IF @backupFileName IS NOT NULL BEGIN 453 | UPDATE [File] SET Name = @backupFileName WHERE UID = @fKey; 454 | SET @fKey = NULL; 455 | END 456 | IF @fKey IS NULL BEGIN 457 | INSERT INTO [File] ( Folder, Name, CreateTime, Description ) 458 | VALUES ( {folderKey}, {secureFileName}, {uploadDate}, {description} ) 459 | SELECT @fKey = SCOPE_IDENTITY(); 460 | END ELSE BEGIN 461 | -- File Existed 462 | UPDATE [File] SET Deleted = 0, Description = ISNULL({description}, Description) WHERE UID = @fKey 463 | END 464 | DECLARE @size int; SET @size = {size}; 465 | -- File was compressed during upload so the 'original' file size is wrong and need to query the length of the content 466 | IF @size IS NULL BEGIN 467 | SELECT @size = DATALENGTH( Content ) 468 | FROM Cache 469 | WHERE UID = {cacheId} 470 | END 471 | INSERT INTO Version ( [File], VersionID, Time, UploadedBy, ContentType, Size, VersionIndex, DataLockerToken ) 472 | VALUES ( @fKey, {version}, {uploadDate}, {user}, {contentType}, @size, 0, {cacheId} ) 473 | INSERT INTO [Log] ( Action, FolderName, FileName, VersionId, VersionIndex, [User], Size, Time ) 474 | VALUES ( 'I', {folder}, {secureFileName}, {version}, 0, {user}, @size, {uploadDate} ) 475 | SELECT @fKey"); 476 | 477 | Assert.AreEqual(@"DECLARE @fKey int; SET @fKey = @p0; 478 | DECLARE @backupFileName VARCHAR(1); SET @backupFileName = @p0; 479 | IF @backupFileName IS NOT NULL BEGIN 480 | UPDATE [File] SET Name = @backupFileName WHERE UID = @fKey; 481 | SET @fKey = NULL; 482 | END 483 | IF @fKey IS NULL BEGIN 484 | INSERT INTO [File] ( Folder, Name, CreateTime, Description ) 485 | VALUES ( @p1, @p2, @p3, @p0 ) 486 | SELECT @fKey = SCOPE_IDENTITY(); 487 | END ELSE BEGIN 488 | -- File Existed 489 | UPDATE [File] SET Deleted = 0, Description = ISNULL(@p0, Description) WHERE UID = @fKey 490 | END 491 | DECLARE @size int; SET @size = @p0; 492 | -- File was compressed during upload so the 'original' file size is wrong and need to query the length of the content 493 | IF @size IS NULL BEGIN 494 | SELECT @size = DATALENGTH( Content ) 495 | FROM Cache 496 | WHERE UID = @p4 497 | END 498 | INSERT INTO Version ( [File], VersionID, Time, UploadedBy, ContentType, Size, VersionIndex, DataLockerToken ) 499 | VALUES ( @fKey, @p4, @p3, @p5, @p6, @size, 0, @p4 ) 500 | INSERT INTO [Log] ( Action, FolderName, FileName, VersionId, VersionIndex, [User], Size, Time ) 501 | VALUES ( 'I', @p7, @p2, @p4, 0, @p5, @size, @p3 ) 502 | SELECT @fKey", query.AsSql().Sql); 503 | 504 | Assert.AreEqual(query.Parameters.Get("p0"), null); 505 | Assert.AreEqual(query.Parameters.Get("p1"), folderKey); 506 | Assert.AreEqual(query.Parameters.Get("p2"), secureFileName); 507 | Assert.AreEqual(query.Parameters.Get("p3"), uploadDate); 508 | Assert.AreEqual(query.Parameters.Get("p4"), cacheId); 509 | Assert.AreEqual(query.Parameters.Get("p5"), user); 510 | Assert.AreEqual(query.Parameters.Get("p6"), contentType); 511 | Assert.AreEqual(query.Parameters.Get("p7"), folder); 512 | } 513 | 514 | [Test] 515 | public void TestRepeatedParameters3() // without leading spaces 516 | { 517 | var cn = new SqlConnection(); 518 | var qb = cn.QueryBuilder($"{"A"}"); 519 | qb.Append($"{"B"}"); 520 | Assert.AreEqual("@p0 @p1", qb.Sql); 521 | } 522 | 523 | [Test] 524 | public void TestRepeatedParameters4() 525 | { 526 | var cn = new SqlConnection(); 527 | var qb = cn.QueryBuilder(); 528 | qb.Append($"{"A"},{"B"},{"C"},{"D"},{"E"},{"F"},{"G"},{"H"},{"I"},{"J"},{"K"},"); // @p0-@p10 529 | qb.Append($"{1},{2},{3},{4},{4},{5},{6},{7},{8},{9},{10},"); // @p10-@p20, with repeated @p14 530 | 531 | qb.Append($"{"A"}"); // @p21 should reuse @p0 532 | qb.Append($"{"B"}"); // @p22 should reuse @p1 533 | 534 | if (InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.ReuseIdenticalParameters) 535 | Assert.AreEqual("@p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,@p11,@p12,@p13,@p14,@p14,@p15,@p16,@p17,@p18,@p19,@p20,@p0 @p1", qb.Sql); 536 | else 537 | Assert.AreEqual("@p0,@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,@p11,@p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19,@p20,@p21,@p22 @p23", qb.Sql); 538 | } 539 | 540 | [Test] 541 | public void TestRepeatedParameters5() 542 | { 543 | var cn = new SqlConnection(); 544 | var qb = cn.QueryBuilder(); 545 | qb.Append($"{"A"}"); // @p0 546 | qb.Append($"{"B"}"); // @p1 547 | 548 | qb.Append($"{2}"); // @p2 549 | qb.Append($"{3}"); // @p3 550 | qb.Append($"{4}"); // @p4 551 | qb.Append($"{5}"); // @p5 552 | qb.Append($"{6}"); // @p6 553 | qb.Append($"{7}"); // @p7 554 | qb.Append($"{8}"); // @p8 555 | qb.Append($"{9}"); // @p9 556 | 557 | qb.Append($"{10}"); // @p10 558 | qb.Append($"{11}"); // @p11 559 | qb.Append($"{12}"); // @p12 560 | qb.Append($"{13}"); // @p13 561 | qb.Append($"{14}"); // @p14 562 | qb.Append($"{15}"); // @p15 563 | qb.Append($"{16}"); // @p16 564 | qb.Append($"{17}"); // @p17 565 | qb.Append($"{18}"); // @p18 566 | qb.Append($"{19}"); // @p19 567 | 568 | qb.Append($"{"A"}"); // @p20 should reuse @p0 569 | qb.Append($"{"B"}"); // @p21 should reuse @p1 570 | 571 | if (InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.ReuseIdenticalParameters) 572 | Assert.AreEqual("@p0 @p1 @p2 @p3 @p4 @p5 @p6 @p7 @p8 @p9 @p10 @p11 @p12 @p13 @p14 @p15 @p16 @p17 @p18 @p19 @p0 @p1", qb.Sql); 573 | else 574 | Assert.AreEqual("@p0 @p1 @p2 @p3 @p4 @p5 @p6 @p7 @p8 @p9 @p10 @p11 @p12 @p13 @p14 @p15 @p16 @p17 @p18 @p19 @p20 @p21", qb.Sql); 575 | } 576 | 577 | [Test] 578 | public void TestMultipleStatements() 579 | { 580 | int orderId = 10; 581 | string currentUserId = "admin"; 582 | 583 | bool softDelete = true; 584 | string action = "DELETED_ORDER"; 585 | string description = $"User {currentUserId} deleted order {orderId}"; 586 | 587 | var cmd = cn.SqlBuilder(); 588 | if (softDelete) 589 | cmd.Append($"UPDATE Orders SET IsDeleted=1 WHERE OrderId = {orderId}; "); 590 | else 591 | cmd.Append($"DELETE FROM Orders WHERE OrderId = {orderId}; "); 592 | cmd.Append($"INSERT INTO Logs (Action, UserId, Description) VALUES ({action}, {orderId}, {description}); "); 593 | 594 | if (InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.ReuseIdenticalParameters) 595 | { 596 | Assert.AreEqual(cmd.Parameters.Count, 3); 597 | Assert.AreEqual(cmd.Parameters.Get("p0"), orderId); 598 | Assert.AreEqual(cmd.Parameters.Get("p1"), action); 599 | Assert.AreEqual(cmd.Parameters.Get("p2"), description); 600 | } 601 | else 602 | { 603 | Assert.AreEqual(cmd.Parameters.Count, 4); 604 | Assert.AreEqual(cmd.Parameters.Get("p0"), orderId); 605 | Assert.AreEqual(cmd.Parameters.Get("p1"), action); 606 | Assert.AreEqual(cmd.Parameters.Get("p2"), orderId); 607 | Assert.AreEqual(cmd.Parameters.Get("p3"), description); 608 | } 609 | } 610 | 611 | [Test] 612 | public void TestQueryBuilderWithJoins() 613 | { 614 | string productName = "%mountain%"; 615 | string joinParam = "test"; 616 | 617 | var query = cn 618 | .QueryBuilder($@"SELECT * FROM [Table1] /**joins**/ WHERE [Table1].[Name] LIKE {productName}"); 619 | 620 | query.From($"INNER JOIN [Table2] on [Table1].Table2Id=[Table2].Id and [Table2].Name={joinParam}"); 621 | 622 | Assert.AreEqual("SELECT * FROM [Table1] INNER JOIN [Table2] on [Table1].Table2Id=[Table2].Id and [Table2].Name=@p1 WHERE [Table1].[Name] LIKE @p0", query.AsSql().Sql); 623 | } 624 | 625 | [Test] 626 | public void TestQueryBuilderWithFrom() 627 | { 628 | string productName = "%mountain%"; 629 | string joinParam = "test"; 630 | 631 | var query = cn 632 | .QueryBuilder($@"SELECT * /**from**/ WHERE [Table1].[Name] LIKE {productName}"); 633 | 634 | query.From($"[Table1]") 635 | .From($"INNER JOIN [Table2] on [Table1].Table2Id=[Table2].Id and [Table2].Name={joinParam}"); 636 | 637 | Assert.AreEqual(@"SELECT * FROM [Table1] 638 | INNER JOIN [Table2] on [Table1].Table2Id=[Table2].Id and [Table2].Name=@p1 WHERE [Table1].[Name] LIKE @p0", query.AsSql().Sql); 639 | } 640 | 641 | [Test] 642 | public void TestQueryBuilderWithSelect() 643 | { 644 | var query = cn.QueryBuilder( 645 | $@"SELECT 646 | * 647 | /**selects**/ 648 | FROM 649 | [Table1]"); 650 | 651 | query.Select($"'Test' as AnotherColumn").Select($"'Test2' as AnotherColumn2"); 652 | 653 | Assert.AreEqual( 654 | @"SELECT 655 | * 656 | , 'Test' as AnotherColumn, 'Test2' as AnotherColumn2 657 | FROM 658 | [Table1]", query.AsSql().Sql); 659 | } 660 | 661 | [Test] 662 | public void ArrayTest() 663 | { 664 | //https://github.com/Drizin/DapperQueryBuilder/issues/22 665 | string v = "a"; 666 | List numList = new List { 1, 2, 3, 4, 5, 6 }; 667 | 668 | FormattableString script = $@" 669 | declare @v1 nvarchar(10)={v} 670 | declare @v2 nvarchar(10)={v} 671 | select 1 from tb where name in {numList} 672 | declare @v3 nvarchar(10)={v} 673 | declare @v4 nvarchar(10)={v} 674 | declare @v5 nvarchar(10)={v} 675 | declare @v6 nvarchar(10)={v} 676 | declare @v7 nvarchar(10)={v} 677 | declare @v8 nvarchar(10)={v} 678 | declare @v9 nvarchar(10)={v} 679 | declare @v10 nvarchar(10)={v} 680 | declare @v11 nvarchar(10)={v} 681 | declare @v12 nvarchar(10)={v} 682 | declare @v13 nvarchar(10)={v} 683 | declare @v14 nvarchar(10)={v} 684 | declare @v15 nvarchar(10)={v} 685 | declare @v16 nvarchar(10)={v} 686 | declare @v17 nvarchar(10)={v} 687 | declare @v18 nvarchar(10)={v} 688 | declare @v19 nvarchar(10)={v} 689 | declare @v20 nvarchar(10)={v} 690 | declare @v21 nvarchar(10)={v} 691 | declare @v22 nvarchar(10)={v} 692 | declare @v23 nvarchar(10)={v} 693 | select 'ok' 694 | "; 695 | 696 | var query = cn.QueryBuilder(script); 697 | var s = query.AsSql().Sql; 698 | var p = query.Parameters; 699 | 700 | if (InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.ReuseIdenticalParameters) 701 | { 702 | Assert.AreEqual(@" 703 | declare @v1 nvarchar(10)=@p0 704 | declare @v2 nvarchar(10)=@p0 705 | select 1 from tb where name in @parray1 706 | declare @v3 nvarchar(10)=@p0 707 | declare @v4 nvarchar(10)=@p0 708 | declare @v5 nvarchar(10)=@p0 709 | declare @v6 nvarchar(10)=@p0 710 | declare @v7 nvarchar(10)=@p0 711 | declare @v8 nvarchar(10)=@p0 712 | declare @v9 nvarchar(10)=@p0 713 | declare @v10 nvarchar(10)=@p0 714 | declare @v11 nvarchar(10)=@p0 715 | declare @v12 nvarchar(10)=@p0 716 | declare @v13 nvarchar(10)=@p0 717 | declare @v14 nvarchar(10)=@p0 718 | declare @v15 nvarchar(10)=@p0 719 | declare @v16 nvarchar(10)=@p0 720 | declare @v17 nvarchar(10)=@p0 721 | declare @v18 nvarchar(10)=@p0 722 | declare @v19 nvarchar(10)=@p0 723 | declare @v20 nvarchar(10)=@p0 724 | declare @v21 nvarchar(10)=@p0 725 | declare @v22 nvarchar(10)=@p0 726 | declare @v23 nvarchar(10)=@p0 727 | select 'ok' 728 | ", query.AsSql().Sql); 729 | 730 | Assert.AreEqual(query.Parameters.Get("p0"), v); 731 | Assert.AreEqual(query.Parameters.Get>("parray1"), numList); 732 | } 733 | else 734 | { 735 | Assert.AreEqual(@" 736 | declare @v1 nvarchar(10)=@p0 737 | declare @v2 nvarchar(10)=@p1 738 | select 1 from tb where name in @parray2 739 | declare @v3 nvarchar(10)=@p3 740 | declare @v4 nvarchar(10)=@p4 741 | declare @v5 nvarchar(10)=@p5 742 | declare @v6 nvarchar(10)=@p6 743 | declare @v7 nvarchar(10)=@p7 744 | declare @v8 nvarchar(10)=@p8 745 | declare @v9 nvarchar(10)=@p9 746 | declare @v10 nvarchar(10)=@p10 747 | declare @v11 nvarchar(10)=@p11 748 | declare @v12 nvarchar(10)=@p12 749 | declare @v13 nvarchar(10)=@p13 750 | declare @v14 nvarchar(10)=@p14 751 | declare @v15 nvarchar(10)=@p15 752 | declare @v16 nvarchar(10)=@p16 753 | declare @v17 nvarchar(10)=@p17 754 | declare @v18 nvarchar(10)=@p18 755 | declare @v19 nvarchar(10)=@p19 756 | declare @v20 nvarchar(10)=@p20 757 | declare @v21 nvarchar(10)=@p21 758 | declare @v22 nvarchar(10)=@p22 759 | declare @v23 nvarchar(10)=@p23 760 | select 'ok' 761 | ", query.AsSql().Sql); 762 | 763 | Assert.AreEqual(query.Parameters.Get("p0"), v); 764 | Assert.AreEqual(query.Parameters.Get("p1"), v); 765 | Assert.AreEqual(query.Parameters.Get>("parray2"), numList); 766 | } 767 | } 768 | 769 | [Test] 770 | public void SimpleQueryStoredProcedure() 771 | { 772 | var q = cn.SqlBuilder($"[dbo].[uspGetEmployeeManagers]") 773 | .AddParameter("BusinessEntityID", 280); 774 | 775 | var r = q.Query(commandType: CommandType.StoredProcedure); 776 | int count = r.Count(); 777 | Assert.That(count > 0); 778 | } 779 | 780 | [Test] 781 | public void QueryMultipleStoredProcedure() 782 | { 783 | //AdventureWorks does not contain a proc which returns multiple result sets, so create our own 784 | int a = cn.SqlBuilder($@" 785 | CREATE OR ALTER PROCEDURE [dbo].[uspGetEmployeeManagers_Twice] 786 | @BusinessEntityID [int] 787 | AS 788 | BEGIN 789 | EXEC [dbo].[uspGetEmployeeManagers] @BusinessEntityID = @BusinessEntityID 790 | EXEC [dbo].[uspGetEmployeeManagers] @BusinessEntityID = @BusinessEntityID 791 | 792 | END").Execute(); 793 | var q = cn.SqlBuilder($"[dbo].[uspGetEmployeeManagers_Twice]") 794 | .AddParameter("BusinessEntityID", 280); 795 | 796 | using (var gridReader = q.QueryMultiple(commandType: CommandType.StoredProcedure)) 797 | { 798 | var r = gridReader.Read(); 799 | int count = r.Count(); 800 | Assert.That(count > 0); 801 | r = gridReader.Read(); 802 | Assert.AreEqual(count, r.Count()); 803 | } 804 | } 805 | 806 | } 807 | } 808 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/DapperQueryBuilder.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | 7 | false 8 | 9 | Rick Drizin 10 | 11 | Rick Drizin 12 | 13 | MIT 14 | 15 | https://github.com/Drizin/DapperQueryBuilder/ 16 | 11.0 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Always 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/ExplicitTypeTests.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Data.SqlClient; 8 | using System.Linq; 9 | using System.Reflection; 10 | 11 | namespace DapperQueryBuilder.Tests 12 | { 13 | public class ExplicitTypeTests 14 | { 15 | UnitTestsDbConnection cn; 16 | 17 | #region Setup 18 | [SetUp] 19 | public void Setup() 20 | { 21 | cn = new UnitTestsDbConnection(new SqlConnection(TestHelper.GetMSSQLConnectionString())); 22 | } 23 | #endregion 24 | 25 | int businessEntityID = 1; 26 | string nationalIDNumber = "295847284"; 27 | DateTime birthDate = new DateTime(1969, 01, 29); 28 | string maritalStatus = "S"; // single 29 | string gender = "M"; 30 | 31 | public class Product 32 | { 33 | public int ProductId { get; set; } 34 | public string Name { get; set; } 35 | } 36 | 37 | [Test] 38 | public void TestExplicitTypes() 39 | { 40 | decimal cost = 884.7083m; 41 | 42 | var cmd1 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [StandardCost]={cost}"); // int32 is matched against DbType.Int32 and will send this dbType to Dapper 43 | Assert.AreEqual("SELECT * FROM [Production].[Product] p WHERE [StandardCost]=@p0", cmd1.Sql); 44 | var products = cmd1.Query(); 45 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.Decimal); 46 | 47 | var cmd2 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [StandardCost]={cost:int32}"); // int32 is matched against DbType.Int32 and will send this dbType to Dapper 48 | Assert.AreEqual("SELECT * FROM [Production].[Product] p WHERE [StandardCost]=@p0", cmd2.Sql); 49 | products = cmd2.Query(); 50 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.Int); 51 | 52 | System.Diagnostics.Debug.WriteLine(cn.PreviousCommands.Last().CommandText); 53 | } 54 | 55 | [Test] 56 | public void TestExplicitTypes2() 57 | { 58 | string productName = "Mountain%"; 59 | 60 | // By default strings are Unicode (nvarchar) and size is max between DbString.DefaultLength (4000) or string 61 | var cmd1 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName}"); 62 | var products = cmd1.Query(); 63 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.NVarChar); 64 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == Dapper.DbString.DefaultLength); 65 | 66 | 67 | // Unless we specify it's an Ansi (non-unicode) string 68 | var cmd2 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName:AnsiString}"); 69 | products = cmd2.Query(); 70 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.VarChar); 71 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == Dapper.DbString.DefaultLength); 72 | 73 | // If string is larger than DbString.DefaultLength (4000), size will be string size 74 | productName = new string('c', 4010); 75 | var cmd3 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName:AnsiString}"); 76 | products = cmd3.Query(); 77 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.VarChar); 78 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == 4010); 79 | } 80 | 81 | [Test] 82 | public void TestExplicitTypes3() 83 | { 84 | string productName = "Mountain%"; 85 | 86 | var cmd1 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName:nvarchar(20)}"); 87 | var products = cmd1.Query(); 88 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.NVarChar); 89 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == 20); 90 | 91 | 92 | var cmd2 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName:varchar(30)}"); 93 | products = cmd2.Query(); 94 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.VarChar); 95 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == 30); 96 | } 97 | 98 | [Test] 99 | public void TestExplicitTypes4() 100 | { 101 | string productName = "Mountain%"; 102 | 103 | var cmd1 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName:nvarchar()}"); 104 | var products = cmd1.Query(); 105 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.NVarChar); 106 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == Dapper.DbString.DefaultLength); 107 | 108 | 109 | var cmd2 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName:varchar()}"); 110 | products = cmd2.Query(); 111 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.VarChar); 112 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == Dapper.DbString.DefaultLength); 113 | } 114 | 115 | [Test] 116 | public void TestExplicitTypes5() 117 | { 118 | string productName = "Mountain%"; 119 | 120 | var cmd1 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName:nchar()}"); 121 | var products = cmd1.Query(); 122 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.NChar); 123 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == productName.Length); 124 | 125 | 126 | var cmd2 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] LIKE {productName:char(20)}"); 127 | products = cmd2.Query(); 128 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.Char); 129 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == 20); 130 | } 131 | 132 | 133 | [Test(Description = "Arrays should allow explicit types")] 134 | public void TestExplicitTypes6() 135 | { 136 | List productNames = new List() 137 | { 138 | "Blade", 139 | "Decal 1", 140 | "Decal 2" 141 | }; 142 | 143 | var cmd1 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] IN {productNames:nvarchar(50)}"); 144 | var products = cmd1.Query(); 145 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.NVarChar); 146 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == 50); 147 | 148 | 149 | var cmd2 = cn.QueryBuilder($"SELECT * FROM [Production].[Product] p WHERE [Name] IN {productNames:varchar(30)}"); 150 | products = cmd2.Query(); 151 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).SqlDbType == SqlDbType.VarChar); 152 | Assert.That(((SqlParameter)cn.PreviousCommands.Last().Parameters.Values.First()).Size == 30); 153 | } 154 | 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/FluentQueryBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql; 2 | using InterpolatedSql.SqlBuilders; 3 | using NUnit.Framework; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Data.SqlClient; 8 | using System.Linq; 9 | 10 | namespace DapperQueryBuilder.Tests 11 | { 12 | public class FluentQueryBuilderTests 13 | { 14 | IDbConnection cn; 15 | 16 | #region Setup 17 | [SetUp] 18 | public void Setup() 19 | { 20 | cn = new SqlConnection(TestHelper.GetMSSQLConnectionString()); 21 | } 22 | #endregion 23 | 24 | string expected = @"SELECT ProductId, Name, ListPrice, Weight 25 | FROM [Production].[Product] 26 | WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2 27 | ORDER BY ProductId"; 28 | 29 | public class Product 30 | { 31 | public int ProductId { get; set; } 32 | public string Name { get; set; } 33 | } 34 | 35 | int maxPrice = 1000; 36 | int maxWeight = 15; 37 | string search = "%Mountain%"; 38 | 39 | [Test] 40 | public void TestFluentAPI() 41 | { 42 | int maxPrice = 1000; 43 | int maxWeight = 15; 44 | string search = "%Mountain%"; 45 | 46 | var q = cn.FluentQueryBuilder() 47 | .Select($"ProductId") 48 | .Select($"Name") 49 | .Select($"ListPrice") 50 | .Select($"Weight") 51 | .From($"[Production].[Product]") 52 | .Where($"[ListPrice] <= {maxPrice}") 53 | .Where($"[Weight] <= {maxWeight}") 54 | .Where($"[Name] LIKE {search}") 55 | .OrderBy($"ProductId") 56 | .Build() 57 | ; 58 | 59 | Assert.AreEqual(expected, q.Sql); 60 | Assert.That(q.DapperParameters.ParameterNames.Contains("p0")); 61 | Assert.That(q.DapperParameters.ParameterNames.Contains("p1")); 62 | Assert.That(q.DapperParameters.ParameterNames.Contains("p2")); 63 | Assert.AreEqual(q.DapperParameters.Get("p0"), maxPrice); 64 | Assert.AreEqual(q.DapperParameters.Get("p1"), maxWeight); 65 | Assert.AreEqual(q.DapperParameters.Get("p2"), search); 66 | 67 | var products = q.Query(); 68 | 69 | Assert.That(products.Any()); 70 | } 71 | 72 | 73 | public class ProductCategories 74 | { 75 | public string Category { get; set; } 76 | public string Subcategory { get; set; } 77 | public string Name { get; set; } 78 | public string ProductNumber { get; set; } 79 | } 80 | 81 | [Test] 82 | public void JoinsTest() 83 | { 84 | var categories = new string[] { "Components", "Clothing", "Acessories" }; 85 | var q = cn.FluentQueryBuilder() 86 | .SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]") 87 | .From($"[Production].[Product] p") 88 | .From($"INNER JOIN [Production].[ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]") 89 | .From($"INNER JOIN [Production].[ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]") 90 | .Where($"c.[Name] IN {categories}").Build(); 91 | var prods = q.Query(); 92 | } 93 | 94 | [Test] 95 | public void FullQueryTest() 96 | { 97 | var q = cn.FluentQueryBuilder() 98 | .Select($"cat.[Name] as [Category]") 99 | .Select($"sc.[Name] as [Subcategory]") 100 | .Select($"AVG(p.[ListPrice]) as [AveragePrice]") 101 | .From($"[Production].[Product] p") 102 | .From($"LEFT JOIN [Production].[ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]") 103 | .From($"LEFT JOIN [Production].[ProductCategory] cat on sc.[ProductCategoryID]=cat.[ProductCategoryID]") 104 | .Where($"p.[ListPrice] BETWEEN { 0 } and { 1000 }") 105 | .Where($"cat.[Name] IS NOT NULL") 106 | .GroupBy($"cat.[Name]") 107 | .GroupBy($"sc.[Name]") 108 | .Having($"COUNT(*)>{5}") 109 | .Build(); 110 | 111 | string expected = 112 | @"SELECT cat.[Name] as [Category], sc.[Name] as [Subcategory], AVG(p.[ListPrice]) as [AveragePrice] 113 | FROM [Production].[Product] p 114 | LEFT JOIN [Production].[ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] 115 | LEFT JOIN [Production].[ProductCategory] cat on sc.[ProductCategoryID]=cat.[ProductCategoryID] 116 | WHERE p.[ListPrice] BETWEEN @p0 and @p1 AND cat.[Name] IS NOT NULL 117 | GROUP BY cat.[Name], sc.[Name] 118 | HAVING COUNT(*)>@p2"; 119 | 120 | Assert.AreEqual(expected, q.Sql); 121 | 122 | var results = q.Query(); 123 | 124 | Assert.That(results.Any()); 125 | 126 | } 127 | 128 | 129 | [Test] 130 | public void TestAndOr() 131 | { 132 | int maxPrice = 1000; 133 | int maxWeight = 15; 134 | string search = "%Mountain%"; 135 | 136 | string expected = @"SELECT ProductId, Name, ListPrice, Weight 137 | FROM [Production].[Product] 138 | WHERE [ListPrice] <= @p0 AND ([Weight] <= @p1 OR [Name] LIKE @p2) 139 | ORDER BY ProductId"; 140 | 141 | var q = cn.FluentQueryBuilder() 142 | .Select($"ProductId") 143 | .Select($"Name") 144 | .Select($"ListPrice") 145 | .Select($"Weight") 146 | .From($"[Production].[Product]") 147 | .Where($"[ListPrice] <= {maxPrice}") 148 | .Where(new Filters(Filters.FiltersType.OR, 149 | $"[Weight] <= {maxWeight}", 150 | $"[Name] LIKE {search}" 151 | )) 152 | .OrderBy($"ProductId") 153 | .Build(); 154 | 155 | Assert.AreEqual(expected, q.Sql); 156 | Assert.That(q.DapperParameters.ParameterNames.Contains("p0")); 157 | Assert.That(q.DapperParameters.ParameterNames.Contains("p1")); 158 | Assert.That(q.DapperParameters.ParameterNames.Contains("p2")); 159 | Assert.AreEqual(q.DapperParameters.Get("p0"), maxPrice); 160 | Assert.AreEqual(q.DapperParameters.Get("p1"), maxWeight); 161 | Assert.AreEqual(q.DapperParameters.Get("p2"), search); 162 | 163 | var products = q.Query(); 164 | 165 | Assert.That(products.Any()); 166 | } 167 | 168 | [Test] 169 | public void TestAndOr2() 170 | { 171 | int minPrice = 200; 172 | int maxPrice = 1000; 173 | int maxWeight = 15; 174 | string search = "%Mountain%"; 175 | 176 | string expected = @"SELECT ProductId, Name, ListPrice, Weight 177 | FROM [Production].[Product] 178 | WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)"; 179 | 180 | var q = cn.FluentQueryBuilder() 181 | .Select($"ProductId, Name, ListPrice, Weight") 182 | .From($"[Production].[Product]") 183 | .Where(new Filters( 184 | $"[ListPrice] >= {minPrice}", 185 | $"[ListPrice] <= {maxPrice}" 186 | )) 187 | .Where(new Filters(Filters.FiltersType.OR, 188 | $"[Weight] <= {maxWeight}", 189 | $"[Name] LIKE {search}" 190 | )) 191 | .Build(); 192 | 193 | Assert.AreEqual(expected, q.Sql); 194 | Assert.That(q.DapperParameters.ParameterNames.Contains("p0")); 195 | Assert.That(q.DapperParameters.ParameterNames.Contains("p1")); 196 | Assert.That(q.DapperParameters.ParameterNames.Contains("p2")); 197 | Assert.That(q.DapperParameters.ParameterNames.Contains("p3")); 198 | Assert.AreEqual(q.DapperParameters.Get("p0"), minPrice); 199 | Assert.AreEqual(q.DapperParameters.Get("p1"), maxPrice); 200 | Assert.AreEqual(q.DapperParameters.Get("p2"), maxWeight); 201 | Assert.AreEqual(q.DapperParameters.Get("p3"), search); 202 | 203 | var products = q.Query(); 204 | } 205 | 206 | [Test] 207 | public void TestDetachedFilters() 208 | { 209 | int minPrice = 200; 210 | int maxPrice = 1000; 211 | int maxWeight = 15; 212 | string search = "%Mountain%"; 213 | 214 | var filters = new Filters(Filters.FiltersType.AND); 215 | filters.Add(new Filters() 216 | { 217 | new Filter($"[ListPrice] >= {minPrice}"), 218 | new Filter($"[ListPrice] <= {maxPrice}") 219 | }); 220 | filters.Add(new Filters(Filters.FiltersType.OR) 221 | { 222 | new Filter($"[Weight] <= {maxWeight}"), 223 | new Filter($"[Name] LIKE {search}") 224 | }); 225 | 226 | var where = filters.Build(); 227 | 228 | Assert.AreEqual(@"WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)", where.Sql); 229 | var parms = where.SqlParameters; 230 | 231 | Assert.AreEqual(4, parms.Count()); 232 | Assert.AreEqual(minPrice, parms[0].Argument); 233 | Assert.AreEqual(maxPrice, parms[1].Argument); 234 | Assert.AreEqual(maxWeight, parms[2].Argument); 235 | Assert.AreEqual(search, parms[3].Argument); 236 | } 237 | 238 | [Test] 239 | public void TestQueryBuilderFluentComposition() 240 | { 241 | 242 | var q = cn.QueryBuilder($"SELECT test FROM test") 243 | .Where($"test") 244 | .Append($"test") // Append on QueryBuilder still returns a QueryBuilder 245 | .Where($"test") 246 | ; 247 | } 248 | 249 | [Test] 250 | public void GroupByOrderByQueryTest() 251 | { 252 | var q = cn.FluentQueryBuilder() 253 | .Select($"cat.[Name] as [Category]") 254 | .Select($"AVG(p.[ListPrice]) as [AveragePrice]") 255 | .From($"[Production].[Product] p") 256 | .From($"LEFT JOIN [Production].[ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]") 257 | .From($"LEFT JOIN [Production].[ProductCategory] cat on sc.[ProductCategoryID]=cat.[ProductCategoryID]") 258 | .Where($"p.[ListPrice] BETWEEN { 0 } and { 1000 }") 259 | .Where($"cat.[Name] IS NOT NULL") 260 | .GroupBy($"cat.[Name]") 261 | .Having($"COUNT(*)>{5}") 262 | .OrderBy($"cat.[Name]") 263 | .Build(); 264 | 265 | string expected = 266 | @"SELECT cat.[Name] as [Category], AVG(p.[ListPrice]) as [AveragePrice] 267 | FROM [Production].[Product] p 268 | LEFT JOIN [Production].[ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] 269 | LEFT JOIN [Production].[ProductCategory] cat on sc.[ProductCategoryID]=cat.[ProductCategoryID] 270 | WHERE p.[ListPrice] BETWEEN @p0 and @p1 AND cat.[Name] IS NOT NULL 271 | GROUP BY cat.[Name] 272 | HAVING COUNT(*)>@p2 273 | ORDER BY cat.[Name]"; 274 | 275 | Assert.AreEqual(expected, q.Sql); 276 | 277 | var results = q.Query(); 278 | 279 | Assert.That(results.Any()); 280 | 281 | } 282 | 283 | [Test] 284 | public void GroupByWithNoWhereTest() 285 | { 286 | var q = cn.FluentQueryBuilder() 287 | .Select($"cat.[Name] as [Category]") 288 | .Select($"AVG(p.[ListPrice]) as [AveragePrice]") 289 | .From($"[Production].[Product] p") 290 | .From($"LEFT JOIN [Production].[ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]") 291 | .From($"LEFT JOIN [Production].[ProductCategory] cat on sc.[ProductCategoryID]=cat.[ProductCategoryID]") 292 | .GroupBy($"cat.[Name]") 293 | .Having($"COUNT(*)>{5}") 294 | .Build(); 295 | 296 | string expected = 297 | @"SELECT cat.[Name] as [Category], AVG(p.[ListPrice]) as [AveragePrice] 298 | FROM [Production].[Product] p 299 | LEFT JOIN [Production].[ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] 300 | LEFT JOIN [Production].[ProductCategory] cat on sc.[ProductCategoryID]=cat.[ProductCategoryID] 301 | GROUP BY cat.[Name] 302 | HAVING COUNT(*)>@p0"; 303 | 304 | Assert.AreEqual(expected, q.Sql); 305 | 306 | var results = q.Query(); 307 | 308 | Assert.That(results.Any()); 309 | 310 | } 311 | 312 | [Test] 313 | public void FluentQueryBuilderInsideCommandBuilder() 314 | { 315 | int maxPrice = 1000; 316 | int maxWeight = 15; 317 | string search = "%Mountain%"; 318 | int customerId = 29825; 319 | 320 | var productsSubQuery = cn.FluentQueryBuilder() 321 | .Select($"ProductId") 322 | .From($"[Production].[Product]") 323 | .Where($"[ListPrice] <= {maxPrice}") 324 | .Where($"[Weight] <= {maxWeight}") 325 | .Where($"[Name] LIKE {search}"); 326 | 327 | short[] statuses = {1, 2, 5}; 328 | var customerOrdersSubQuery = cn.FluentQueryBuilder() 329 | .Select($"SalesOrderID") 330 | .From($"[Sales].[SalesOrderHeader]") 331 | .Where($"[CustomerId] = {customerId}") 332 | .Where($"[Status] IN {statuses}"); 333 | 334 | var finalQuery = cn 335 | .QueryBuilder($"SELECT * FROM [Sales].[SalesOrderDetail]") 336 | .Append($"WHERE [ProductId] IN ({productsSubQuery})") 337 | .Append($"AND [SalesOrderId] IN ({customerOrdersSubQuery})"); 338 | 339 | string expected = 340 | @"SELECT * FROM [Sales].[SalesOrderDetail] WHERE [ProductId] IN (SELECT ProductId 341 | FROM [Production].[Product] 342 | WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2) AND [SalesOrderId] IN (SELECT SalesOrderID 343 | FROM [Sales].[SalesOrderHeader] 344 | WHERE [CustomerId] = @p3 AND [Status] IN @parray4)"; 345 | 346 | Assert.AreEqual(expected, finalQuery.Sql); 347 | Assert.That(finalQuery.Parameters.ParameterNames.Contains("p0")); 348 | Assert.That(finalQuery.Parameters.ParameterNames.Contains("p1")); 349 | Assert.That(finalQuery.Parameters.ParameterNames.Contains("p2")); 350 | Assert.That(finalQuery.Parameters.ParameterNames.Contains("p3")); 351 | Assert.That(finalQuery.Parameters.ParameterNames.Contains("parray4")); 352 | Assert.AreEqual(finalQuery.Parameters.Get("p0"), maxPrice); 353 | Assert.AreEqual(finalQuery.Parameters.Get("p1"), maxWeight); 354 | Assert.AreEqual(finalQuery.Parameters.Get("p2"), search); 355 | Assert.AreEqual(finalQuery.Parameters.Get("p3"), customerId); 356 | Assert.AreEqual(finalQuery.Parameters.Get("parray4"), statuses); 357 | 358 | var orderItems = finalQuery.Query(); 359 | 360 | Assert.That(orderItems.Any()); 361 | } 362 | 363 | [Test] 364 | public void TestQueryBuilderWithNestedQueryBuilder() 365 | { 366 | string val1 = "val1"; 367 | string val2 = "val2"; 368 | var condition = cn.QueryBuilder($"col3 = {val2}"); 369 | 370 | var q = cn.QueryBuilder($@"SELECT col1, {val1} as col2 FROM Table1 WHERE {condition}"); 371 | 372 | Assert.AreEqual("SELECT col1, @p0 as col2 FROM Table1 WHERE col3 = @p1", q.Sql); 373 | 374 | Assert.AreEqual(2, q.Parameters.Count); 375 | Assert.AreEqual(val1, q.Parameters["p0"].Value); 376 | Assert.AreEqual(val2, q.Parameters["p1"].Value); 377 | } 378 | 379 | 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/Helpers/UnitTestsDbCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Text; 5 | 6 | namespace DapperQueryBuilder.Tests 7 | { 8 | /// 9 | /// This is just a wrapper around IDbCommand, which allows us to inspect how Dapper is preparing our Commands 10 | /// 11 | public class UnitTestsDbCommand : IDbCommand 12 | { 13 | private readonly IDbCommand _cmd; 14 | private readonly UnitTestsDbConnection _ownerConnection; 15 | public UnitTestsDbCommand(IDbCommand command, UnitTestsDbConnection ownerConnection) 16 | { 17 | _cmd = command; 18 | _ownerConnection = ownerConnection; 19 | } 20 | 21 | public string CommandText { get => _cmd.CommandText; set => _cmd.CommandText = value; } 22 | public int CommandTimeout { get => _cmd.CommandTimeout; set => _cmd.CommandTimeout = value; } 23 | public CommandType CommandType { get => _cmd.CommandType; set => _cmd.CommandType = value; } 24 | public IDbConnection Connection { get => _cmd.Connection; set => _cmd.Connection = value; } 25 | 26 | public IDataParameterCollection Parameters => _cmd.Parameters; 27 | 28 | public IDbTransaction Transaction { get => _cmd.Transaction; set => _cmd.Transaction = value; } 29 | public UpdateRowSource UpdatedRowSource { get => _cmd.UpdatedRowSource; set => _cmd.UpdatedRowSource = value; } 30 | 31 | public void Cancel() 32 | { 33 | _cmd.Cancel(); 34 | } 35 | 36 | public IDbDataParameter CreateParameter() 37 | { 38 | return _cmd.CreateParameter(); 39 | } 40 | 41 | public void Dispose() 42 | { 43 | _cmd.Dispose(); 44 | } 45 | 46 | public int ExecuteNonQuery() 47 | { 48 | SaveCurrentCommand(); 49 | return _cmd.ExecuteNonQuery(); 50 | } 51 | 52 | public IDataReader ExecuteReader() 53 | { 54 | SaveCurrentCommand(); 55 | return _cmd.ExecuteReader(); 56 | } 57 | 58 | public IDataReader ExecuteReader(CommandBehavior behavior) 59 | { 60 | SaveCurrentCommand(); 61 | return _cmd.ExecuteReader(behavior); 62 | } 63 | 64 | public object ExecuteScalar() 65 | { 66 | SaveCurrentCommand(); 67 | return _cmd.ExecuteScalar(); 68 | } 69 | 70 | public void Prepare() 71 | { 72 | SaveCurrentCommand(); 73 | _cmd.Prepare(); 74 | } 75 | 76 | private void SaveCurrentCommand() 77 | { 78 | var cmdDetails = new UnitTestsDbConnection.ExecutedCommandDetails() { CommandText = _cmd.CommandText }; 79 | foreach (var parm in _cmd.Parameters) 80 | cmdDetails.Parameters.Add(((IDbDataParameter)parm).ParameterName, parm); 81 | _ownerConnection.PreviousCommands.Add(cmdDetails); 82 | } 83 | 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/Helpers/UnitTestsDbConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Diagnostics; 5 | using System.Text; 6 | 7 | namespace DapperQueryBuilder.Tests 8 | { 9 | /// 10 | /// This is just a wrapper around IDbConnection, which allows us to inspect how Dapper is preparing our Commands 11 | /// 12 | public class UnitTestsDbConnection : IDbConnection 13 | { 14 | private readonly IDbConnection _conn; 15 | 16 | // Since Dapper clears the parameters of IDbCommands after their execution we have to store a copy of the information instead of storing the IDbCommand itself 17 | public List PreviousCommands { get; set; } = new List(); 18 | 19 | public UnitTestsDbConnection(IDbConnection connection) 20 | { 21 | _conn = connection; 22 | } 23 | 24 | public string ConnectionString { get => _conn.ConnectionString; set => _conn.ConnectionString = value; } 25 | 26 | public int ConnectionTimeout => _conn.ConnectionTimeout; 27 | 28 | public string Database => _conn.Database; 29 | 30 | public ConnectionState State => _conn.State; 31 | 32 | public IDbTransaction BeginTransaction() 33 | { 34 | return _conn.BeginTransaction(); 35 | } 36 | 37 | public IDbTransaction BeginTransaction(IsolationLevel il) 38 | { 39 | return _conn.BeginTransaction(il); 40 | } 41 | 42 | public void ChangeDatabase(string databaseName) 43 | { 44 | _conn.ChangeDatabase(databaseName); 45 | } 46 | 47 | public void Close() 48 | { 49 | _conn.Close(); 50 | } 51 | 52 | public IDbCommand CreateCommand() 53 | { 54 | return new UnitTestsDbCommand(_conn.CreateCommand(), this); 55 | } 56 | 57 | public void Dispose() 58 | { 59 | _conn.Dispose(); 60 | } 61 | 62 | public void Open() 63 | { 64 | _conn.Open(); 65 | } 66 | 67 | [DebuggerDisplay("{CommandText}")] 68 | public class ExecutedCommandDetails 69 | { 70 | public string CommandText { get; set; } 71 | public Dictionary Parameters { get; set; } = new Dictionary(); 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/MySQLTests.cs: -------------------------------------------------------------------------------- 1 | using MySql.Data.MySqlClient; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Data.OleDb; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace DapperQueryBuilder.Tests 11 | { 12 | public class MySQLTests 13 | { 14 | UnitTestsDbConnection cn; 15 | 16 | #region Setup 17 | [SetUp] 18 | public void Setup() 19 | { 20 | cn = new UnitTestsDbConnection(new MySqlConnection(TestHelper.GetMySQLConnectionString())); 21 | } 22 | #endregion 23 | 24 | public class Author 25 | { 26 | public int author_id { get; set; } 27 | public string name_last { get; set; } 28 | public string name_first { get; set; } 29 | public string country { get; set; } 30 | } 31 | 32 | [Test] 33 | public void TestConnection() 34 | { 35 | var authors = Dapper.SqlMapper.Query(cn, "SELECT * FROM authors"); 36 | Assert.That(authors.Any()); 37 | } 38 | 39 | [Test] 40 | public void TestParameters() 41 | { 42 | string search = "%as%"; 43 | var authors = cn.QueryBuilder($"SELECT * FROM authors WHERE name_last like {search}").Query(); 44 | Assert.That(authors.Any()); 45 | Assert.AreEqual(cn.PreviousCommands.Last().CommandText, "SELECT * FROM authors WHERE name_last like @p0"); 46 | } 47 | 48 | [Test] 49 | public void TestArrays() 50 | { 51 | List lastNames = new List() 52 | { 53 | "Kafka", 54 | "de Assis", 55 | }; 56 | 57 | var authors = cn.QueryBuilder($"SELECT * FROM authors WHERE name_last IN {lastNames}").Query(); 58 | Assert.That(authors.Any()); 59 | Assert.AreEqual(cn.PreviousCommands.Last().CommandText, "SELECT * FROM authors WHERE name_last IN (@parray01,@parray02)"); 60 | } 61 | 62 | [Test] 63 | public void TestNullableArrays() 64 | { 65 | int[]? ids = new[] { 1, 2 }; 66 | 67 | var authors = cn.QueryBuilder($"SELECT * FROM authors WHERE author_id IN {ids}").Query(); 68 | Assert.That(authors.Any()); 69 | 70 | string[]? lastNames = new string[] 71 | { 72 | "Kafka", 73 | "de Assis", 74 | }; 75 | 76 | authors = cn.QueryBuilder($"SELECT * FROM authors WHERE name_last IN {lastNames}").Query(); 77 | Assert.AreEqual(cn.PreviousCommands.Last().CommandText, "SELECT * FROM authors WHERE name_last IN (@parray01,@parray02)"); 78 | 79 | AuthorsEnum[]? authorIds = new AuthorsEnum[] { AuthorsEnum.Kafka, AuthorsEnum.MachadoDeAssis }; 80 | authors = cn.QueryBuilder($"SELECT * FROM authors WHERE author_id IN {authorIds}").Query(); 81 | Assert.That(authors.Any()); 82 | Assert.AreEqual(cn.PreviousCommands.Last().CommandText, "SELECT * FROM authors WHERE author_id IN (@parray01,@parray02)"); 83 | 84 | } 85 | 86 | enum AuthorsEnum 87 | { 88 | Kafka = 1, 89 | MachadoDeAssis = 2, 90 | } 91 | 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/PostgreSQLTests.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql; 2 | using Npgsql; 3 | using NUnit.Framework; 4 | using System.Data; 5 | using System.Linq; 6 | 7 | namespace DapperQueryBuilder.Tests 8 | { 9 | public class PostgreSQLTests 10 | { 11 | IDbConnection cn; 12 | 13 | #region Setup 14 | [SetUp] 15 | public void Setup() 16 | { 17 | cn = new NpgsqlConnection(TestHelper.GetPostgreSQLConnectionString()); 18 | } 19 | [TearDown] 20 | public void TearDown() 21 | { 22 | // reverting back for next unit tests 23 | InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.DatabaseParameterSymbol = "@"; 24 | InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.AutoGeneratedParameterPrefix = "p"; 25 | } 26 | #endregion 27 | 28 | public class TitleRecord 29 | { 30 | public int Film_ID { get; set; } 31 | public string Title { get; set; } 32 | public string Description { get; set; } 33 | public int Release_Year { get; set; } 34 | } 35 | 36 | [Test] 37 | public void TestConnection() 38 | { 39 | var titles = Dapper.SqlMapper.Query(cn, "SELECT * FROM film"); 40 | Assert.That(titles.Any()); 41 | } 42 | 43 | [Test] 44 | public void TestParameters() 45 | { 46 | // Npgsql will rewrite SQL query and convert @p0, @p1, etc to positional placeholders ($1, $2)... https://stackoverflow.com/a/49544098/3606250 47 | string search = "%Dinosaur%"; 48 | var titles = cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}").Query(); 49 | Assert.That(titles.Any()); 50 | } 51 | 52 | [Test] 53 | public void TestAutoGeneratedParameterPrefix() 54 | { 55 | // Npgsql does NOT require this, but it may be required in some databases/drivers which do not accept "at-parameters" (@p0, @p1, etc). 56 | InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.DatabaseParameterSymbol = ":"; 57 | InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.AutoGeneratedParameterPrefix = "parm"; 58 | 59 | string search = "%Dinosaur%"; 60 | var cmd = cn.QueryBuilder($"SELECT * FROM film WHERE title like {search}"); 61 | 62 | Assert.AreEqual("SELECT * FROM film WHERE title like :parm0", cmd.Sql); 63 | } 64 | 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/QueryBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Data; 5 | using System.Data.SqlClient; 6 | using System.Linq; 7 | 8 | namespace DapperQueryBuilder.Tests 9 | { 10 | public class QueryBuilderTests 11 | { 12 | IDbConnection cn; 13 | 14 | #region Setup 15 | [SetUp] 16 | public void Setup() 17 | { 18 | cn = new SqlConnection(TestHelper.GetMSSQLConnectionString()); 19 | } 20 | #endregion 21 | 22 | string expected = @"SELECT ProductId, Name, ListPrice, Weight 23 | FROM [Production].[Product] 24 | WHERE [ListPrice] <= @p0 AND [Weight] <= @p1 AND [Name] LIKE @p2 25 | ORDER BY ProductId 26 | "; 27 | 28 | public class Product 29 | { 30 | public int ProductId { get; set; } 31 | public string Name { get; set; } 32 | } 33 | 34 | int maxPrice = 1000; 35 | int maxWeight = 15; 36 | string search = "%Mountain%"; 37 | 38 | [Test] 39 | public void TestOperatorOverload() 40 | { 41 | string search = "%mountain%"; 42 | var cmd = cn.QueryBuilder() 43 | + $@"SELECT * FROM [Production].[Product]" 44 | + $"WHERE [Name] LIKE {search}"; 45 | cmd += $"AND 1=1"; 46 | Assert.AreEqual("SELECT * FROM [Production].[Product] WHERE [Name] LIKE @p0 AND 1=1", cmd.Sql); 47 | } 48 | 49 | [Test] 50 | public void TestTemplateAPI() 51 | { 52 | 53 | var q = cn.QueryBuilder( 54 | $@"SELECT ProductId, Name, ListPrice, Weight 55 | FROM [Production].[Product] 56 | /**where**/ 57 | ORDER BY ProductId 58 | "); 59 | q.Where($"[ListPrice] <= {maxPrice}"); 60 | q.Where($"[Weight] <= {maxWeight}"); 61 | q.Where($"[Name] LIKE {search}"); 62 | 63 | Assert.AreEqual(expected, q.Sql); 64 | Assert.That(q.Parameters.ParameterNames.Contains("p0")); 65 | Assert.That(q.Parameters.ParameterNames.Contains("p1")); 66 | Assert.That(q.Parameters.ParameterNames.Contains("p2")); 67 | Assert.AreEqual(q.Parameters.Get("p0"), maxPrice); 68 | Assert.AreEqual(q.Parameters.Get("p1"), maxWeight); 69 | Assert.AreEqual(q.Parameters.Get("p2"), search); 70 | 71 | var products = q.Query(); 72 | 73 | Assert.That(products.Any()); 74 | } 75 | 76 | public class ProductCategories 77 | { 78 | public string Category { get; set; } 79 | public string Subcategory { get; set; } 80 | public string Name { get; set; } 81 | public string ProductNumber { get; set; } 82 | } 83 | 84 | 85 | 86 | [Test] 87 | public void TestDetachedFilters() 88 | { 89 | int minPrice = 200; 90 | int maxPrice = 1000; 91 | int maxWeight = 15; 92 | string search = "%Mountain%"; 93 | 94 | var filters = new Filters(Filters.FiltersType.AND); 95 | filters.Add(new Filters() 96 | { 97 | new Filter($"[ListPrice] >= {minPrice}"), 98 | new Filter($"[ListPrice] <= {maxPrice}") 99 | }); 100 | filters.Add(new Filters(Filters.FiltersType.OR) 101 | { 102 | new Filter($"[Weight] <= {maxWeight}"), 103 | new Filter($"[Name] LIKE {search}") 104 | }); 105 | 106 | var where = filters.Build(); 107 | 108 | Assert.AreEqual(@"WHERE ([ListPrice] >= @p0 AND [ListPrice] <= @p1) AND ([Weight] <= @p2 OR [Name] LIKE @p3)", where.Sql); 109 | var parms = where.SqlParameters; 110 | 111 | Assert.AreEqual(4, parms.Count()); 112 | Assert.AreEqual(minPrice, parms[0].Argument); 113 | Assert.AreEqual(maxPrice, parms[1].Argument); 114 | Assert.AreEqual(maxWeight, parms[2].Argument); 115 | Assert.AreEqual(search, parms[3].Argument); 116 | } 117 | 118 | [Test] 119 | public void TestQueryBuilderWithNestedFormattableString() 120 | { 121 | int orgId = 123; 122 | FormattableString innerQuery = $"SELECT Id, Name FROM SomeTable where OrganizationId={orgId}"; 123 | var q = cn.QueryBuilder($"SELECT FROM ({innerQuery}) a join AnotherTable b on a.Id=b.Id where b.OrganizationId={321}"); 124 | 125 | Assert.AreEqual("SELECT FROM (SELECT Id, Name FROM SomeTable where OrganizationId=@p0) a join AnotherTable b on a.Id=b.Id where b.OrganizationId=@p1", q.Sql); 126 | 127 | Assert.AreEqual(2, q.Parameters.Count); 128 | var p0 = q.Parameters["p0"]; 129 | var p1 = q.Parameters["p1"]; 130 | Assert.AreEqual(123, p0.Value); 131 | Assert.AreEqual(321, p1.Value); 132 | } 133 | 134 | [Test] 135 | public void TestQueryBuilderWithNestedFormattableString2() 136 | { 137 | int orgId = 123; 138 | FormattableString otherColumns = $"{"111111111"} AS {"ssn":raw}"; 139 | FormattableString innerQuery = $"SELECT Id, Name, {otherColumns} FROM SomeTable where OrganizationId={orgId}"; 140 | var q = cn.QueryBuilder($"SELECT FROM ({innerQuery}) a join AnotherTable b on a.Id=b.Id where b.OrganizationId={321}"); 141 | 142 | Assert.AreEqual("SELECT FROM (SELECT Id, Name, @p0 AS ssn FROM SomeTable where OrganizationId=@p1) a join AnotherTable b on a.Id=b.Id where b.OrganizationId=@p2", q.Sql); 143 | 144 | Assert.AreEqual(3, q.Parameters.Count); 145 | Assert.AreEqual("111111111", q.Parameters["p0"].Value); 146 | Assert.AreEqual(123, q.Parameters["p1"].Value); 147 | Assert.AreEqual(321, q.Parameters["p2"].Value); 148 | } 149 | 150 | [Test] 151 | public void TestQueryBuilderWithNestedFormattableString3() 152 | { 153 | string val1 = "val1"; 154 | string val2 = "val2"; 155 | FormattableString condition = $"col3 = {val2}"; 156 | 157 | var q = cn.QueryBuilder($@"SELECT col1, {val1} as col2 FROM Table1 WHERE {condition}"); 158 | 159 | Assert.AreEqual("SELECT col1, @p0 as col2 FROM Table1 WHERE col3 = @p1", q.Sql); 160 | 161 | Assert.AreEqual(2, q.Parameters.Count); 162 | Assert.AreEqual(val1, q.Parameters["p0"].Value); 163 | Assert.AreEqual(val2, q.Parameters["p1"].Value); 164 | } 165 | 166 | [Test] 167 | public void TestMultipleFilterExtensions() 168 | { 169 | var storageFolder = "_CALCENGINES"; 170 | var storageFolder2 = "_CALCENGINES"; 171 | var sqlTestNamePattern = "'%~_Test.%' ESCAPE '~'"; 172 | var regularNames = new[] { "Conduent_1_CE" }; 173 | 174 | var qb = cn.QueryBuilder(@$" 175 | SELECT COUNT(*) TotalCount 176 | FROM [File] f 177 | INNER JOIN Folder fo ON f.Folder = fo.UID 178 | WHERE fo.Name = {storageFolder:varchar(200)} AND f.Name NOT LIKE {sqlTestNamePattern:raw} AND f.Deleted != 1 /**filters**/ 179 | 180 | SELECT COUNT(*) TotalCount 181 | FROM [File] f 182 | INNER JOIN Folder fo ON f.Folder = fo.UID 183 | WHERE fo.Name = {storageFolder:varchar(200)} AND f.Name NOT LIKE {sqlTestNamePattern:raw} AND f.Deleted != 1 /**filters**/ 184 | "); 185 | 186 | var filters = new Filters(Filters.FiltersType.AND) 187 | { 188 | new Filter($"f.Name IN {regularNames:varchar(200)}") 189 | }; 190 | 191 | qb.Where(filters); 192 | 193 | Assert.AreEqual(@" 194 | SELECT COUNT(*) TotalCount 195 | FROM [File] f 196 | INNER JOIN Folder fo ON f.Folder = fo.UID 197 | WHERE fo.Name = @p0 AND f.Name NOT LIKE '%~_Test.%' ESCAPE '~' AND f.Deleted != 1 AND f.Name IN @parray2 198 | 199 | SELECT COUNT(*) TotalCount 200 | FROM [File] f 201 | INNER JOIN Folder fo ON f.Folder = fo.UID 202 | WHERE fo.Name = @p1 AND f.Name NOT LIKE '%~_Test.%' ESCAPE '~' AND f.Deleted != 1 AND f.Name IN @parray3 203 | ", qb.Sql); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/Setup-MSSQL.sql: -------------------------------------------------------------------------------- 1 | /*********** How to setup a MSSQL for tests ***********/ 2 | 3 | -- 1) Download https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2019.bak 4 | -- 2) Find your DATA FOLDER (if it's not C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\) using this query: 5 | /* 6 | SELECT TOP 1 7 | substring(f.physical_name, 1, len(f.physical_name)-charindex('\', reverse(f.physical_name))+1) 8 | from sys.master_files f INNER JOIN sys.databases db ON f.database_id=db.database_id 9 | order by len ( substring(f.physical_name, 1, len(f.physical_name)-charindex('\', reverse(f.physical_name))+1) ) 10 | */ 11 | -- Replace folders below with the correct locations. 12 | 13 | RESTORE DATABASE [AdventureWorks2019] FROM DISK = N'C:\Users\youruser\Downloads\AdventureWorks2019.bak' WITH FILE=1, 14 | MOVE 'AdventureWorks2017' TO 'C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\AdventureWorks2019.mdf', 15 | MOVE 'AdventureWorks2017_log' TO 'C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\AdventureWorks2019.ldf' 16 | GO 17 | 18 | -- 3) Adjust Connection string in TestSettings.json 19 | -- e.g. remove "\SQLEXPRESS", and/or change "(local)" by some hostname 20 | -- e.g. replace "Integrated Security=True" by "User Id=username;Password=password" 21 | 22 | -- 4) Create some extra objects (AdventureWorks does not have everything we need): 23 | USE [AdventureWorks2019] 24 | GO 25 | 26 | CREATE PROCEDURE [sp_TestOutput] 27 | @Input1 [int], 28 | @Output1 [int] OUTPUT 29 | AS 30 | BEGIN 31 | SET @Output1 = 2 32 | END; 33 | GO 34 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/Setup-MySQL.sql: -------------------------------------------------------------------------------- 1 | /*********** How to setup a MySQL for tests ***********/ 2 | 3 | -- 1) Download MariaDB for Windows https://dlm.mariadb.com/browse/mariadb_server/200/1374/winx64-packages/ 4 | 5 | CREATE DATABASE DapperQueryBuilderTests; 6 | 7 | USE DapperQueryBuilderTests; 8 | 9 | CREATE TABLE authors 10 | (author_id INT AUTO_INCREMENT PRIMARY KEY, 11 | name_last VARCHAR(50), 12 | name_first VARCHAR(50), 13 | country VARCHAR(50) ); 14 | 15 | INSERT INTO authors 16 | (name_last, name_first, country) 17 | VALUES('Kafka', 'Franz', 'Czech Republic'); 18 | 19 | INSERT INTO authors 20 | (name_last, name_first, country) 21 | VALUES('de Assis', 'Machado', 'Brazil'); 22 | 23 | 24 | CREATE TABLE books ( 25 | isbn CHAR(20) PRIMARY KEY, 26 | title VARCHAR(50), 27 | author_id INT, 28 | publisher_id INT, 29 | year_pub CHAR(4), 30 | description TEXT ); 31 | 32 | INSERT INTO books 33 | (title, author_id, isbn, year_pub) 34 | VALUES('The Castle', '1', '0805211063', '1998'); 35 | 36 | INSERT INTO books 37 | (title, author_id, isbn, year_pub) 38 | VALUES('The Trial', '1', '0805210407', '1995'), 39 | ('The Metamorphosis', '1', '0553213695', '1995'), 40 | ('America', '1', '0805210644', '1995'); 41 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace DapperQueryBuilder.Tests 7 | { 8 | public class TestHelper 9 | { 10 | public static IConfiguration Configuration { get; set; } 11 | static TestHelper() 12 | { 13 | Configuration = new ConfigurationBuilder() 14 | .AddJsonFile("TestSettings.json") 15 | .Build(); 16 | } 17 | public static string GetMSSQLConnectionString() => Configuration.GetConnectionString("MSSQLConnection"); 18 | public static string GetPostgreSQLConnectionString() => Configuration.GetConnectionString("PostgreSQLConnection"); 19 | public static string GetMySQLConnectionString() => Configuration.GetConnectionString("MySQLConnection"); 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder.Tests/TestSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | // AdventureWorks 2019 - https://docs.microsoft.com/en-us/sql/samples/adventureworks-install-configure?view=sql-server-ver15) 3 | "ConnectionStrings": { 4 | "MSSQLConnection": "Data Source=(local);Initial Catalog=AdventureWorks2019;Integrated Security=True;", 5 | "PostgreSQLConnection": "User ID=postgres;Password=myPassword;Host=localhost;Port=5432;Database=dvdrental;", 6 | "MySQLConnection": "Server=localhost;Database=DapperQueryBuilderTests;Uid=root;Pwd=myPassword;" 7 | } 8 | } -------------------------------------------------------------------------------- /src/DapperQueryBuilder.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32505.173 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperQueryBuilder", "DapperQueryBuilder\DapperQueryBuilder.csproj", "{8CF86804-9246-4B3D-A1E9-78DDCF758DBC}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperQueryBuilder.Tests", "DapperQueryBuilder.Tests\DapperQueryBuilder.Tests.csproj", "{42D7E39A-D7CC-404B-B67A-87AF33183670}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{88CCCDA2-06FC-42EF-8AAF-D92250C285AD}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {8CF86804-9246-4B3D-A1E9-78DDCF758DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {8CF86804-9246-4B3D-A1E9-78DDCF758DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {8CF86804-9246-4B3D-A1E9-78DDCF758DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {8CF86804-9246-4B3D-A1E9-78DDCF758DBC}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {42D7E39A-D7CC-404B-B67A-87AF33183670}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {42D7E39A-D7CC-404B-B67A-87AF33183670}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {42D7E39A-D7CC-404B-B67A-87AF33183670}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {42D7E39A-D7CC-404B-B67A-87AF33183670}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {AB589EF0-8B1A-4791-A8B5-F3ED178AF34F} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/Dapper-QueryBuilder.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dapper-QueryBuilder 5 | DapperQueryBuilder 6 | Rick Drizin 7 | Rick Drizin 8 | MIT 9 | https://github.com/Drizin/DapperQueryBuilder 10 | false 11 | DapperQueryBuilder: Dapper Query Builder using String Interpolation and Fluent API 12 | Copyright Rick Drizin 2020 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/DapperQueryBuilder.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net462;net472;net5.0;net6.0;net7.0 5 | Rick Drizin 6 | MIT 7 | https://github.com/Drizin/DapperQueryBuilder/ 8 | Dapper Query Builder using Fluent API and String Interpolation 9 | Rick Drizin 10 | Rick Drizin 11 | 2.0.0 12 | false 13 | DapperQueryBuilder 14 | Dapper-QueryBuilder 15 | DapperQueryBuilder.xml 16 | dapper;query-builder;query builder;dapperquerybuilder;dapper-query-builder;dapper-interpolation;dapper-interpolated-string 17 | true 18 | true 19 | 20 | NuGetReadMe.md 21 | DapperQueryBuilder 22 | enable 23 | 8.0 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Style", "IDE0008:Use explicit type", Justification = "var avoids noisy code")] 9 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/IDapperSqlCommand.cs: -------------------------------------------------------------------------------- 1 | namespace InterpolatedSql.Dapper 2 | { 3 | /// 4 | /// Dapper Sql Command that can be executed 5 | /// 6 | public interface IDapperSqlCommand : ISqlCommand 7 | { 8 | /// Sql Parameters converted into Dapper format 9 | ParametersDictionary DapperParameters { get; } 10 | } 11 | 12 | /// 13 | /// Dapper Sql Command that can be executed 14 | /// 15 | public interface IDapperSqlCommand : ISqlCommand, IDapperSqlCommand 16 | where T : IDapperSqlCommand, ISqlCommand 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/IDbConnectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql.SqlBuilders; 2 | using System; 3 | using System.Data; 4 | using SqlBuilder = InterpolatedSql.Dapper.SqlBuilders.SqlBuilder; 5 | using QueryBuilder = InterpolatedSql.Dapper.SqlBuilders.QueryBuilder; 6 | using InterpolatedSql.Dapper.SqlBuilders; 7 | using InterpolatedSql; 8 | 9 | namespace DapperQueryBuilder 10 | { 11 | /// 12 | /// Extends IDbConnection to easily build QueryBuilder or SqlBuilder 13 | /// 14 | public static partial class IDbConnectionExtensions 15 | { 16 | public static InterpolatedSql.Dapper.SqlBuilderFactory SqlBuilderFactory { get; set; } = InterpolatedSql.Dapper.SqlBuilderFactory.Default; 17 | 18 | #region SqlBuilder 19 | /// 20 | /// Creates a new IInterpolatedSqlBuilder of type B over current connection 21 | /// 22 | public static B SqlBuilder(this IDbConnection cnn) 23 | where B : IDapperSqlBuilder 24 | { 25 | return SqlBuilderFactory.Create(cnn); 26 | } 27 | 28 | #if NET6_0_OR_GREATER 29 | /// 30 | /// Creates a new IInterpolatedSqlBuilder of type B over current connection 31 | /// 32 | /// SQL command 33 | public static B SqlBuilder(this IDbConnection cnn, ref InterpolatedSqlHandler command) 34 | where B : IDapperSqlBuilder 35 | { 36 | if (command.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) 37 | command.AdjustMultilineString(); 38 | return SqlBuilderFactory.Create(cnn, command.InterpolatedSqlBuilder.AsFormattableString()); 39 | } 40 | 41 | /// 42 | /// Creates a new SqlBuilder over current connection 43 | /// 44 | /// SQL command 45 | public static SqlBuilder SqlBuilder(this IDbConnection cnn, ref InterpolatedSqlHandler command) 46 | { 47 | if (command.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) 48 | command.AdjustMultilineString(); 49 | return new SqlBuilder(cnn, command.InterpolatedSqlBuilder.AsFormattableString()); 50 | } 51 | 52 | /// 53 | /// Creates a new IInterpolatedSqlBuilder of type B over current connection 54 | /// 55 | /// SQL command 56 | public static B SqlBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("options")] ref InterpolatedSqlHandler command) 57 | where B : IDapperSqlBuilder 58 | { 59 | if (command.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) 60 | command.AdjustMultilineString(); 61 | return SqlBuilderFactory.Create(cnn, command.InterpolatedSqlBuilder.AsFormattableString()); 62 | } 63 | 64 | /// 65 | /// Creates a new SqlBuilder over current connection 66 | /// 67 | /// SQL command 68 | public static SqlBuilder SqlBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("options")] ref InterpolatedSqlHandler command) 69 | { 70 | if (command.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) 71 | command.AdjustMultilineString(); 72 | return new SqlBuilder(cnn, command.InterpolatedSqlBuilder.AsFormattableString()); 73 | } 74 | 75 | #else 76 | /// 77 | /// Creates a new IInterpolatedSqlBuilder of type B over current connection 78 | /// 79 | /// SQL command 80 | public static B SqlBuilder(this IDbConnection cnn, FormattableString command) 81 | where B : IDapperSqlBuilder 82 | { 83 | return SqlBuilderFactory.Create(cnn, command); 84 | } 85 | 86 | /// 87 | /// Creates a new SqlBuilder over current connection 88 | /// 89 | /// SQL command 90 | public static SqlBuilder SqlBuilder(this IDbConnection cnn, FormattableString command) 91 | { 92 | return new SqlBuilder(cnn, command); 93 | } 94 | 95 | /// 96 | /// Creates a new IInterpolatedSqlBuilder of type B over current connection 97 | /// 98 | /// SQL command 99 | public static B SqlBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, FormattableString command) 100 | where B : IDapperSqlBuilder 101 | { 102 | return SqlBuilderFactory.Create(cnn, command, options); 103 | } 104 | 105 | /// 106 | /// Creates a new SqlBuilder over current connection 107 | /// 108 | /// SQL command 109 | public static SqlBuilder SqlBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, FormattableString command) 110 | { 111 | return new SqlBuilder(cnn, command, options); 112 | } 113 | #endif 114 | 115 | /// 116 | /// Creates a new empty SqlBuilder over current connection 117 | /// 118 | public static SqlBuilder SqlBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions? options = null) 119 | { 120 | return new SqlBuilder(cnn, options); 121 | } 122 | #endregion 123 | 124 | #region QueryBuilder 125 | /// 126 | /// Creates a new QueryBuilder over current connection 127 | /// 128 | /// You can use "{where}" or "/**where**/" in your query, and it will be replaced by "WHERE + filters" (if any filter is defined).
129 | /// You can use "{filters}" or "/**filters**/" in your query, and it will be replaced by "filters" (without where) (if any filter is defined). 130 | /// 131 | public static QueryBuilder QueryBuilder(this IDbConnection cnn, FormattableString query) 132 | { 133 | return new QueryBuilder(cnn, query); 134 | } 135 | 136 | /// 137 | /// Creates a new empty QueryBuilder over current connection 138 | /// 139 | public static QueryBuilder QueryBuilder(this IDbConnection cnn) 140 | { 141 | return new QueryBuilder(cnn); 142 | } 143 | #endregion 144 | 145 | #region (Legacy) CommandBuilder 146 | #if NET6_0_OR_GREATER 147 | /// 148 | /// Creates a new SqlBuilder over current connection 149 | /// 150 | /// SQL command 151 | public static SqlBuilder CommandBuilder(this IDbConnection cnn, ref InterpolatedSqlHandler command) 152 | { 153 | if (command.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) 154 | command.AdjustMultilineString(); 155 | return new SqlBuilder(cnn, command.InterpolatedSqlBuilder.AsFormattableString()); 156 | } 157 | 158 | /// 159 | /// Creates a new SqlBuilder over current connection 160 | /// 161 | /// SQL command 162 | public static SqlBuilder CommandBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, [System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("options")] ref InterpolatedSqlHandler command) 163 | { 164 | if (command.InterpolatedSqlBuilder.Options.AutoAdjustMultilineString) 165 | command.AdjustMultilineString(); 166 | return new SqlBuilder(cnn, command.InterpolatedSqlBuilder.AsFormattableString()); 167 | } 168 | 169 | #else 170 | /// 171 | /// Creates a new SqlBuilder over current connection 172 | /// 173 | /// SQL command 174 | public static SqlBuilder CommandBuilder(this IDbConnection cnn, FormattableString command) 175 | { 176 | return new SqlBuilder(cnn, command); 177 | } 178 | 179 | /// 180 | /// Creates a new SqlBuilder over current connection 181 | /// 182 | /// SQL command 183 | public static SqlBuilder CommandBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions options, FormattableString command) 184 | { 185 | return new SqlBuilder(cnn, command, options); 186 | } 187 | #endif 188 | 189 | /// 190 | /// Creates a new empty SqlBuilder over current connection 191 | /// 192 | public static SqlBuilder CommandBuilder(this IDbConnection cnn, InterpolatedSqlBuilderOptions? options = null) 193 | { 194 | return new SqlBuilder(cnn, options); 195 | } 196 | #endregion 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/ImmutableDapperCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Data; 3 | 4 | namespace InterpolatedSql.Dapper 5 | { 6 | /// 7 | /// Immutable implementation of . 8 | /// 9 | 10 | public class ImmutableDapperCommand : ImmutableInterpolatedSql, IDapperSqlCommand 11 | { 12 | /// 13 | /// Database connection associated to the command 14 | /// 15 | public IDbConnection DbConnection { get; set; } 16 | 17 | /// Sql Parameters converted into Dapper format 18 | public ParametersDictionary DapperParameters { get; } 19 | 20 | /// 21 | public ImmutableDapperCommand(IDbConnection connection, 22 | string sql, string format, IReadOnlyList sqlParameters, IReadOnlyList explicitParameters) : base(sql, format, sqlParameters, explicitParameters) 23 | { 24 | DbConnection = connection; 25 | DapperParameters = ParametersDictionary.LoadFrom(this); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/InterpolatedSqlDapperOptions.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql; 2 | 3 | namespace InterpolatedSql.Dapper 4 | { 5 | /// 6 | /// Global Options 7 | /// 8 | public class InterpolatedSqlDapperOptions 9 | { 10 | /// 11 | /// Responsible for parsing SqlParameters (see ) 12 | /// into a list of SqlParameterInfo that 13 | /// 14 | public static SqlParameterMapper InterpolatedSqlParameterParser = new SqlParameterMapper(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/NuGetReadMe.md: -------------------------------------------------------------------------------- 1 | # Dapper Query Builder 2 | 3 | **Dapper Query Builder using String Interpolation and Fluent API** 4 | 5 | This library is a wrapper around Dapper mostly for helping building dynamic SQL queries and commands. 6 | 7 | ## Parameters are passed using String Interpolation (but it's safe against SQL injection!) 8 | 9 | By using interpolated strings we can pass parameters directly (embedded in the query) without having to use anonymous objects and without worrying about matching the property names with the SQL parameters. We can just build our queries with regular string interpolation and this library **will automatically "parameterize" our interpolated objects (sql-injection safe)**. 10 | 11 | ```cs 12 | var products = cn 13 | .QueryBuilder($@" 14 | SELECT * FROM Product 15 | WHERE 16 | Name LIKE {productName} 17 | AND ProductSubcategoryID = {subCategoryId} 18 | ORDER BY ProductId").Query; 19 | ``` 20 | 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. 21 | 22 | ## Query and Parameters walk side-by-side 23 | 24 | QueryBuilder basically wraps 2 things that should always stay together: the query which you're building, and the parameters which must go together with our query. 25 | This is a simple concept but it allows us to dynamically add new parameterized SQL clauses/conditions in a single statement. 26 | 27 | **Let's say you're building a query with a variable number of conditions**: 28 | ```cs 29 | var query = cn.QueryBuilder($"SELECT * FROM Product WHERE 1=1"); 30 | query += $"AND Name LIKE {productName}"; 31 | query += $"AND ProductSubcategoryID = {subCategoryId}"; 32 | var products = query.Query(); 33 | ``` 34 | QueryBuilder will wrap both the Query and the Parameters, so that you can easily append new sql statements (and parameters) easily. 35 | When you invoke Query, the underlying query and parameters are passed to Dapper. 36 | 37 | 38 | 39 | ## Static Query 40 | 41 | ```cs 42 | // Create a QueryBuilder with a static query. 43 | // QueryBuilder will automatically convert interpolated parameters to Dapper parameters (injection-safe) 44 | var q = cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM Product 45 | WHERE ListPrice <= {maxPrice}"; 46 | ORDER BY ProductId"); 47 | 48 | // Query() will automatically pass our query and injection-safe SqlParameters to Dapper 49 | var products = q.Query(); 50 | // all other Dapper extensions are also available: QueryAsync, QueryMultiple, ExecuteScalar, etc.. 51 | ``` 52 | 53 | So, basically you pass parameters as interpolated strings, but they are converted to safe SqlParameters. 54 | 55 | This is our mojo :-) 56 | 57 | ## Dynamic Query 58 | 59 | One of the top reasons for dynamically building SQL statements is to dynamically append new filters (`where` statements). 60 | 61 | ```cs 62 | // create a QueryBuilder with initial query 63 | var q = cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight FROM Product WHERE 1=1"); 64 | 65 | // Dynamically append whatever statements you need, and QueryBuilder will automatically 66 | // convert interpolated parameters to Dapper parameters (injection-safe) 67 | q += $"AND ListPrice <= {maxPrice}"; 68 | q += $"AND Weight <= {maxWeight}"; 69 | q += $"AND Name LIKE {search}"; 70 | q += $"ORDER BY ProductId"; 71 | 72 | var products = q.Query(); 73 | ``` 74 | 75 | ## Static Command 76 | 77 | ```cs 78 | var cmd = cn.SqlBuilder($"DELETE FROM Orders WHERE OrderId = {orderId};"); 79 | int deletedRows = cmd.Execute(); 80 | ``` 81 | 82 | ```cs 83 | cn.SqlBuilder($@" 84 | INSERT INTO Product (ProductName, ProductSubCategoryId) 85 | VALUES ({productName}, {ProductSubcategoryID}) 86 | ").Execute(); 87 | ``` 88 | 89 | 90 | ## Command with Multiple statements 91 | 92 | In a single roundtrip we can run multiple SQL commands: 93 | 94 | ```cs 95 | var cmd = cn.SqlBuilder(); 96 | cmd += $"DELETE FROM Orders WHERE OrderId = {orderId}; "; 97 | cmd += $"INSERT INTO Logs (Action, UserId, Description) VALUES ({action}, {orderId}, {description}); "; 98 | cmd.Execute(); 99 | ``` 100 | 101 | 102 | ## Dynamic Query with \*\*where\*\* keyword 103 | 104 | If you don't like the idea of using `WHERE 1=1` (even though it [doesn't hurt performance](https://dba.stackexchange.com/a/33958/85815)), you can use the special keyword **\*\*where\*\*** that act as a placeholder to render dynamically-defined filters. 105 | 106 | `QueryBuilder` maintains an internal list of filters (property called `Filters`) which keeps track of all filters you've added using `.Where()` method. 107 | Then, when `QueryBuilder` invokes Dapper and sends the underlying query it will search for the keyword `/**where**/` in our query and if it exists it will replace it with the filters added (if any), combined using `AND` statements. 108 | 109 | 110 | Example: 111 | 112 | ```cs 113 | // We can write the query structure and use QueryBuilder to render the "where" filters (if any) 114 | var q = cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight 115 | FROM Product 116 | /**where**/ 117 | ORDER BY ProductId 118 | "); 119 | 120 | // You just pass the parameters as if it was an interpolated string, 121 | // and QueryBuilder will automatically convert them to Dapper parameters (injection-safe) 122 | q.Where($"ListPrice <= {maxPrice}"); 123 | q.Where($"Weight <= {maxWeight}"); 124 | q.Where($"Name LIKE {search}"); 125 | 126 | // Query() will automatically render your query and replace /**where**/ keyword (if any filter was added) 127 | var products = q.Query(); 128 | 129 | // In this case Dapper would get "WHERE ListPrice <= @p0 AND Weight <= @p1 AND Name LIKE @p2" and the associated values 130 | ``` 131 | 132 | When Dapper is invoked we replace the `/**where**/` by `WHERE AND AND ` (if any filter was added). 133 | 134 | ## Dynamic Query with \*\*filters\*\* keyword 135 | 136 | **\*\*filters\*\*** is exactly like **\*\*where\*\***, but it's used if we already have other fixed conditions before: 137 | ```cs 138 | var q = cn.QueryBuilder(@"SELECT ProductId, Name, ListPrice, Weight 139 | FROM Product 140 | WHERE Price>{minPrice} /**filters**/ 141 | ORDER BY ProductId 142 | "); 143 | ``` 144 | 145 | When Dapper is invoked we replace the `/**filters**/` by `AND AND ` (if any filter was added). 146 | 147 | 148 | ## IN lists 149 | 150 | Dapper allows us to use IN lists magically. And it also works with our string interpolation: 151 | 152 | ```cs 153 | var q = cn.QueryBuilder($@" 154 | SELECT c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber] 155 | FROM [Product] p 156 | INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID] 157 | INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]"); 158 | 159 | var categories = new string[] { "Components", "Clothing", "Acessories" }; 160 | q += $"WHERE c.[Name] IN {categories}"; 161 | ``` 162 | 163 | 164 | 165 | ## Fluent API (Chained-methods) 166 | 167 | ```cs 168 | var q = cn.FluentQueryBuilder() 169 | .Select($"ProductId") 170 | .Select($"Name") 171 | .Select($"ListPrice") 172 | .Select($"Weight") 173 | .From($"[Product]") 174 | .Where($"[ListPrice] <= {maxPrice}") 175 | .Where($"[Weight] <= {maxWeight}") 176 | .Where($"[Name] LIKE {search}") 177 | .OrderBy($"ProductId"); 178 | 179 | var products = q.Query(); 180 | ``` 181 | 182 | Building joins dynamically using Fluent API: 183 | 184 | ```cs 185 | var categories = new string[] { "Components", "Clothing", "Acessories" }; 186 | 187 | var q = cn.QueryBuilder() 188 | .SelectDistinct($"c.[Name] as [Category], sc.[Name] as [Subcategory], p.[Name], p.[ProductNumber]") 189 | .From($"[Product] p") 190 | .From($"INNER JOIN [ProductSubcategory] sc ON p.[ProductSubcategoryID]=sc.[ProductSubcategoryID]") 191 | .From($"INNER JOIN [ProductCategory] c ON sc.[ProductCategoryID]=c.[ProductCategoryID]") 192 | .Where($"c.[Name] IN {categories}"); 193 | ``` 194 | 195 | There are also chained-methods for adding GROUP BY, HAVING, ORDER BY, and paging (OFFSET x ROWS / FETCH NEXT x ROWS ONLY). 196 | 197 | 198 | 199 | See full documentation [here](https://github.com/Drizin/DapperQueryBuilder/) 200 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilderFactory.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql.Dapper.SqlBuilders; 2 | using InterpolatedSql.SqlBuilders; 3 | using System; 4 | using System.Data; 5 | using SqlBuilder = InterpolatedSql.Dapper.SqlBuilders.SqlBuilder; 6 | 7 | namespace InterpolatedSql.Dapper 8 | { 9 | /// 10 | /// Creates 11 | /// 12 | public class SqlBuilderFactory 13 | { 14 | /// 15 | /// Creates a new IInterpolatedSqlBuilderBase of type B 16 | /// 17 | public virtual B Create(IDbConnection connection) 18 | where B : IDapperSqlBuilder 19 | { 20 | var ctor = typeof(B).GetConstructor(new Type[] { typeof(IDbConnection) }); 21 | B builder = (B)ctor.Invoke(new object[] { connection }); 22 | return builder; 23 | } 24 | 25 | /// 26 | /// Creates a new IInterpolatedSqlBuilderBase of type B 27 | /// 28 | public virtual B Create(IDbConnection connection, InterpolatedSqlBuilderOptions options) 29 | where B : IDapperSqlBuilder 30 | { 31 | var ctor = typeof(B).GetConstructor(new Type[] { typeof(IDbConnection), typeof(InterpolatedSqlBuilderOptions) }); 32 | B builder = (B)ctor.Invoke(new object[] { connection, options }); 33 | return builder; 34 | } 35 | 36 | /// 37 | /// Creates the default IInterpolatedSqlBuilder, which by default is SqlBuilder 38 | /// 39 | public virtual SqlBuilder Create(IDbConnection connection, InterpolatedSqlBuilderOptions? options = null) 40 | { 41 | SqlBuilder builder = new SqlBuilder(connection, options); 42 | return builder; 43 | } 44 | 45 | /// 46 | /// Creates a new IInterpolatedSqlBuilderBase of type B 47 | /// 48 | public virtual B Create(IDbConnection connection, FormattableString command) 49 | where B : IDapperSqlBuilder 50 | { 51 | var ctor = typeof(B).GetConstructor(new Type[] { typeof(IDbConnection), typeof(FormattableString) }); 52 | B builder = (B)ctor.Invoke(new object[] { connection, command }); 53 | return builder; 54 | } 55 | 56 | /// 57 | /// Creates a new IInterpolatedSqlBuilderBase of type B 58 | /// 59 | public virtual B Create(IDbConnection connection, FormattableString command, InterpolatedSqlBuilderOptions? options = null) 60 | where B : IDapperSqlBuilder 61 | { 62 | var ctor = typeof(B).GetConstructor(new Type[] { typeof(IDbConnection), typeof(FormattableString), typeof(InterpolatedSqlBuilderOptions) }); 63 | B builder = (B)ctor.Invoke(new object[] { connection, command, options }); 64 | return builder; 65 | } 66 | 67 | #if NET6_0_OR_GREATER 68 | /// 69 | /// Creates a new IInterpolatedSqlBuilderBase of type B 70 | /// 71 | public virtual B Create(IDbConnection connection, int literalLength, int formattedCount) 72 | where B : IDapperSqlBuilder 73 | { 74 | var ctor = typeof(B).GetConstructor(new Type[] { typeof(IDbConnection), typeof(int), typeof(int) }); 75 | B builder = (B)ctor.Invoke(new object[] { connection, literalLength, formattedCount }); 76 | return builder; 77 | } 78 | 79 | /// 80 | /// Creates a new IInterpolatedSqlBuilder of type B 81 | /// 82 | public virtual B Create(IDbConnection connection, int literalLength, int formattedCount, InterpolatedSqlBuilderOptions? options = null) 83 | where B : IDapperSqlBuilder 84 | { 85 | var ctor = typeof(B).GetConstructor(new Type[] { typeof(IDbConnection), typeof(int), typeof(int), typeof(InterpolatedSqlBuilderOptions) }); 86 | B builder = (B)ctor.Invoke(new object?[] { connection, literalLength, formattedCount, options }); 87 | return builder; 88 | } 89 | 90 | /// 91 | /// Creates new SqlBuilder 92 | /// 93 | public virtual SqlBuilder Create(IDbConnection connection, int literalLength, int formattedCount, InterpolatedSqlBuilderOptions? options = null) 94 | { 95 | SqlBuilder builder = new SqlBuilder(connection, literalLength, formattedCount, options); 96 | return builder; 97 | } 98 | #endif 99 | 100 | 101 | /// 102 | /// Default Factory 103 | /// 104 | public static SqlBuilderFactory Default = new SqlBuilderFactory(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/FluentQueryBuilder/FluentQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql.SqlBuilders; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Text; 6 | 7 | namespace InterpolatedSql.Dapper.SqlBuilders.FluentQueryBuilder 8 | { 9 | /// 10 | /// Exactly like 11 | /// (an injection-safe dynamic SQL builder with a Fluent API that helps to build the query step by step) 12 | /// but also wraps an underlying IDbConnection, and there are extensions to invoke Dapper methods 13 | /// 14 | public class FluentQueryBuilder : global::InterpolatedSql.SqlBuilders.FluentQueryBuilder.FluentQueryBuilder, 15 | IFluentQueryBuilder, 16 | IBuildable, 17 | IDapperSqlBuilder 18 | { 19 | #region ctors 20 | /// 21 | public FluentQueryBuilder( 22 | Func combinedBuilderFactory1, 23 | Func?, SqlBuilder> combinedBuilderFactory2, 24 | IDbConnection connection) : base(combinedBuilderFactory1, combinedBuilderFactory2, connection) 25 | { 26 | Options.CalculateAutoParameterName = (parameter, pos) => InterpolatedSqlDapperOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); 27 | } 28 | #endregion 29 | 30 | #region Overrides 31 | /// 32 | /// Associated DbConnection 33 | /// 34 | public new IDbConnection DbConnection 35 | { 36 | get => base.DbConnection!; 37 | set => base.DbConnection = value; 38 | } 39 | #endregion 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/FluentQueryBuilder/IDbConnectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using InterpolatedSql.SqlBuilders.FluentQueryBuilder; 3 | using InterpolatedSql.Dapper.SqlBuilders.FluentQueryBuilder; 4 | using InterpolatedSql.Dapper.SqlBuilders; 5 | using InterpolatedSql.Dapper; 6 | namespace DapperQueryBuilder 7 | { 8 | /// 9 | /// Extends IDbConnection to easily build FluentQueryBuilder 10 | /// 11 | public static partial class IDbConnectionExtensions 12 | { 13 | /// 14 | /// Creates a new empty FluentQueryBuilder over current connection 15 | /// 16 | /// 17 | public static IEmptyQueryBuilder< 18 | InterpolatedSql.Dapper.SqlBuilders.FluentQueryBuilder.IFluentQueryBuilder, 19 | SqlBuilder, 20 | IDapperSqlCommand 21 | > FluentQueryBuilder(this IDbConnection cnn) 22 | { 23 | return new FluentQueryBuilder((options) => new SqlBuilder(cnn, options), (opts, format, arguments) => new SqlBuilder(cnn, opts, format, arguments), cnn); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/FluentQueryBuilder/IFluentQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace InterpolatedSql.Dapper.SqlBuilders.FluentQueryBuilder 4 | { 5 | public interface IFluentQueryBuilder 6 | : InterpolatedSql.SqlBuilders.FluentQueryBuilder.IFluentQueryBuilder, 7 | IQueryBuilder, 8 | InterpolatedSql.SqlBuilders.ISqlBuilder, 9 | IBuildable 10 | { 11 | //ParametersDictionary DapperParameters { get; } 12 | IDbConnection DbConnection { get; set; } 13 | 14 | IDapperSqlCommand Build(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/IDapperSqlBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace InterpolatedSql.Dapper.SqlBuilders 2 | { 3 | /// 4 | /// Any Builder that creates a 5 | /// 6 | public interface IDapperSqlBuilder : InterpolatedSql.SqlBuilders.IInterpolatedSqlBuilderBase 7 | { 8 | /// 9 | /// Builds the SQL statement 10 | /// 11 | IDapperSqlCommand Build(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/InsertUpdateBuilder/IDbConnectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace InterpolatedSql.Dapper.SqlBuilders.InsertUpdateBuilder 4 | { 5 | /// 6 | /// Extends IDbConnection to easily build InsertUpdateBuilder 7 | /// 8 | public static partial class IDbConnectionExtensions 9 | { 10 | /// 11 | /// Creates a new empty InsertUpdateBuilder over current connection 12 | /// 13 | public static InsertUpdateBuilder InsertUpdateBuilder(this IDbConnection cnn, string tableName) 14 | { 15 | return new InsertUpdateBuilder(tableName, cnn); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/InsertUpdateBuilder/IInsertUpdateBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace InterpolatedSql.Dapper.SqlBuilders.InsertUpdateBuilder 2 | { 3 | public interface IInsertUpdateBuilder : InterpolatedSql.SqlBuilders.InsertUpdateBuilder.IInsertUpdateBuilder 4 | where U : IInsertUpdateBuilder, IBuildable 5 | where RB : IDapperSqlBuilder, IBuildable 6 | where R : class, IInterpolatedSql, IDapperSqlCommand 7 | { 8 | } 9 | public interface IInsertUpdateBuilder : IInsertUpdateBuilder 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/InsertUpdateBuilder/InsertUpdateBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | 4 | namespace InterpolatedSql.Dapper.SqlBuilders.InsertUpdateBuilder 5 | { 6 | /// 7 | public class InsertUpdateBuilder : InsertUpdateBuilder, IInsertUpdateBuilder, IDapperSqlBuilder 8 | { 9 | #region ctors 10 | /// 11 | public InsertUpdateBuilder(string tableName, IDbConnection connection) : base(tableName, opts => new SqlBuilder(connection, opts), connection) 12 | { 13 | } 14 | #endregion 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/InsertUpdateBuilder/InsertUpdateBuilder{U,RB,R}.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql.SqlBuilders; 2 | using System; 3 | using System.Data; 4 | 5 | namespace InterpolatedSql.Dapper.SqlBuilders.InsertUpdateBuilder 6 | { 7 | /// 8 | /// Exactly like but also wraps a (required) underlying IDbConnection, 9 | /// has a "Filters" property which can track a list of filters which are later combined (by default with AND) and will replace the keyword /**where**/, 10 | /// provides facades (as extension-methods) to invoke Dapper extensions (see ), 11 | /// and maps and 12 | /// into Dapper type. 13 | /// 14 | public abstract class InsertUpdateBuilder : global::InterpolatedSql.SqlBuilders.InsertUpdateBuilder.InsertUpdateBuilder, IInsertUpdateBuilder 15 | where U : IInsertUpdateBuilder, ISqlBuilder, IBuildable 16 | where RB : IDapperSqlBuilder, IBuildable 17 | where R : class, IInterpolatedSql, IDapperSqlCommand 18 | { 19 | #region ctors 20 | /// 21 | protected InsertUpdateBuilder(string tableName, Func combinedBuilderFactory, IDbConnection connection) : base(tableName, combinedBuilderFactory) 22 | { 23 | DbConnection = connection; 24 | Options.CalculateAutoParameterName = (parameter, pos) => InterpolatedSqlDapperOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); 25 | } 26 | #endregion 27 | 28 | #region Overrides 29 | /// 30 | /// Associated DbConnection 31 | /// 32 | public new IDbConnection DbConnection 33 | { 34 | get => base.DbConnection!; 35 | set => base.DbConnection = value; 36 | } 37 | #endregion 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/QueryBuilder/Filter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DapperQueryBuilder 4 | { 5 | public class Filter : InterpolatedSql.SqlBuilders.Filter 6 | { 7 | /// 8 | /// New Filter statement.
9 | /// Example: $"[CategoryId] = {categoryId}"
10 | /// Example: $"[Name] LIKE {productName}" 11 | ///
12 | public Filter(FormattableString filter) : base(filter) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/QueryBuilder/Filters.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql.SqlBuilders; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace DapperQueryBuilder 7 | { 8 | public class Filters : InterpolatedSql.SqlBuilders.Filters 9 | { 10 | #region ctor 11 | 12 | /// 13 | /// Create a new group of filters. 14 | /// 15 | public Filters(FiltersType type, IEnumerable filters) 16 | { 17 | Type = type; 18 | this.AddRange(filters); 19 | } 20 | 21 | /// 22 | /// Create a new group of filters which are combined with AND operator. 23 | /// 24 | public Filters(IEnumerable filters) : this(FiltersType.AND, filters) 25 | { 26 | } 27 | 28 | /// 29 | /// Create a new group of filters from formattable strings 30 | /// 31 | public Filters(FiltersType type, params FormattableString[] filters) : 32 | this(type, filters.Select(fiString => new Filter(fiString))) 33 | { 34 | } 35 | 36 | /// 37 | /// Create a new group of filters from formattable strings which are combined with AND operator. 38 | /// 39 | public Filters(params FormattableString[] filters) : this(FiltersType.AND, filters) 40 | { 41 | } 42 | #endregion 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/QueryBuilder/IQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace InterpolatedSql.Dapper.SqlBuilders 2 | { 3 | public interface IQueryBuilder : InterpolatedSql.SqlBuilders.IQueryBuilder 4 | where U : IQueryBuilder, IBuildable 5 | where RB : IDapperSqlBuilder, IBuildable 6 | where R : class, IInterpolatedSql, IDapperSqlCommand 7 | { 8 | } 9 | public interface IQueryBuilder : IQueryBuilder 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/QueryBuilder/QueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | 4 | namespace InterpolatedSql.Dapper.SqlBuilders 5 | { 6 | /// 7 | public class QueryBuilder : QueryBuilder, IQueryBuilder, IDapperSqlBuilder 8 | { 9 | #region ctors 10 | /// 11 | public QueryBuilder(IDbConnection connection) : base(opts => new SqlBuilder(connection, opts), (opts, format, arguments) => new SqlBuilder(connection, opts, format, arguments), connection) 12 | { 13 | } 14 | 15 | /// 16 | public QueryBuilder(IDbConnection connection, FormattableString query) : base(opts => new SqlBuilder(connection, opts), (opts, format, arguments) => new SqlBuilder(connection, opts, format, arguments), connection, query) 17 | { 18 | } 19 | #endregion 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/QueryBuilder/QueryBuilder{U,RB,R}.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql.SqlBuilders; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Text; 6 | 7 | namespace InterpolatedSql.Dapper.SqlBuilders 8 | { 9 | /// 10 | /// Exactly like but also wraps a (required) underlying IDbConnection, 11 | /// has a "Filters" property which can track a list of filters which are later combined (by default with AND) and will replace the keyword /**where**/, 12 | /// provides facades (as extension-methods) to invoke Dapper extensions (see ), 13 | /// and maps and 14 | /// into Dapper type. 15 | /// 16 | public abstract class QueryBuilder : global::InterpolatedSql.SqlBuilders.QueryBuilder, IQueryBuilder 17 | where U : IQueryBuilder, ISqlBuilder, IBuildable 18 | where RB : IDapperSqlBuilder, IBuildable 19 | where R : class, IInterpolatedSql, IDapperSqlCommand 20 | { 21 | #region ctors 22 | /// 23 | protected QueryBuilder( 24 | Func combinedBuilderFactory1, 25 | Func?, RB> combinedBuilderFactory2, 26 | IDbConnection connection 27 | ) : base(combinedBuilderFactory1, combinedBuilderFactory2) 28 | { 29 | DbConnection = connection; 30 | Options.CalculateAutoParameterName = (parameter, pos) => InterpolatedSqlDapperOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); 31 | } 32 | 33 | /// 34 | protected QueryBuilder( 35 | Func combinedBuilderFactory1, 36 | Func?, RB> combinedBuilderFactory2, 37 | IDbConnection connection, 38 | FormattableString query 39 | ) : base(combinedBuilderFactory1, combinedBuilderFactory2, query) 40 | { 41 | DbConnection = connection; 42 | Options.CalculateAutoParameterName = (parameter, pos) => InterpolatedSqlDapperOptions.InterpolatedSqlParameterParser.CalculateAutoParameterName(parameter, pos, base.Options); 43 | } 44 | #endregion 45 | 46 | #region Overrides 47 | /// 48 | /// Associated DbConnection 49 | /// 50 | public new IDbConnection DbConnection 51 | { 52 | get => base.DbConnection!; 53 | set => base.DbConnection = value; 54 | } 55 | #endregion 56 | 57 | #region Legacy compatibility (DapperQueryBuilder) 58 | public new string Sql => base.Build().Sql; 59 | 60 | public QueryBuilderParameters Parameters => new QueryBuilderParameters(this); 61 | public class QueryBuilderParameters 62 | { 63 | public HashSet ParameterNames; 64 | protected ParametersDictionary _dapperParameters; 65 | public QueryBuilderParameters(QueryBuilder builder) 66 | { 67 | _dapperParameters = builder.Build().DapperParameters; 68 | ParameterNames = _dapperParameters.ParameterNames; 69 | } 70 | public T Get(string parameterName) 71 | { 72 | return _dapperParameters.Get(parameterName); 73 | } 74 | public SqlParameterInfo this[string parameterName] 75 | { 76 | get { return _dapperParameters[parameterName]; } 77 | } 78 | public int Count => _dapperParameters.Count; 79 | } 80 | #endregion 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/SqlBuilder/ISqlBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace InterpolatedSql.Dapper.SqlBuilders 2 | { 3 | public interface ISqlBuilder : InterpolatedSql.SqlBuilders.ISqlBuilder, IDapperSqlBuilder 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/SqlBuilder/SqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql.SqlBuilders; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Text; 6 | 7 | namespace InterpolatedSql.Dapper.SqlBuilders 8 | { 9 | /// 10 | public class SqlBuilder : SqlBuilder, ISqlBuilder, IDapperSqlBuilder 11 | { 12 | #region ctors 13 | /// 14 | protected internal SqlBuilder(IDbConnection connection, InterpolatedSqlBuilderOptions? options, StringBuilder? format, List? arguments) : base(connection, options, format, arguments) 15 | { 16 | DbConnection = connection; 17 | } 18 | 19 | /// 20 | public SqlBuilder(IDbConnection connection, InterpolatedSqlBuilderOptions? options = null) : base(connection, options) 21 | { 22 | DbConnection = connection; 23 | } 24 | 25 | 26 | /// 27 | public SqlBuilder(IDbConnection connection, FormattableString value, InterpolatedSqlBuilderOptions? options = null) : base(connection, value, options) 28 | { 29 | DbConnection = connection; 30 | } 31 | 32 | #if NET6_0_OR_GREATER 33 | /// 34 | public SqlBuilder(IDbConnection connection, int literalLength, int formattedCount, InterpolatedSqlBuilderOptions? options = null) : base(connection, literalLength, formattedCount, options) 35 | { 36 | DbConnection = connection; 37 | } 38 | #endif 39 | #endregion 40 | 41 | #region Overrides 42 | /// 43 | public override IDapperSqlCommand Build() 44 | { 45 | return this.ToDapperSqlCommand(); 46 | } 47 | 48 | /// 49 | /// Like 50 | /// 51 | /// 52 | public IDapperSqlCommand ToDapperSqlCommand() 53 | { 54 | string format = _format.ToString(); 55 | return new ImmutableDapperCommand(this.DbConnection, BuildSql(format, _sqlParameters), format, _sqlParameters, _explicitParameters); 56 | } 57 | 58 | 59 | #endregion 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlBuilders/SqlBuilder/SqlBuilder{U,RB,R}.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedSql.SqlBuilders; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Text; 6 | 7 | namespace InterpolatedSql.Dapper.SqlBuilders 8 | { 9 | /// 10 | /// Exactly like but also wraps a (required) underlying IDbConnection, 11 | /// has a "Filters" property which can track a list of filters which are later combined (by default with AND) and will replace the keyword /**where**/, 12 | /// provides facades (as extension-methods) to invoke Dapper extensions (see ), 13 | /// and maps and 14 | /// into Dapper type. 15 | /// 16 | public abstract class SqlBuilder : global::InterpolatedSql.SqlBuilders.SqlBuilder 17 | where U : ISqlBuilder 18 | where R : class, IInterpolatedSql, IDapperSqlCommand 19 | { 20 | 21 | #region ctors 22 | /// 23 | protected SqlBuilder(IDbConnection connection, InterpolatedSqlBuilderOptions? options, StringBuilder? format, List? arguments) : base(options, format, arguments) 24 | { 25 | DbConnection = connection; 26 | } 27 | 28 | /// 29 | public SqlBuilder(IDbConnection connection, InterpolatedSqlBuilderOptions? options = null) : base(options) 30 | { 31 | DbConnection = connection; 32 | } 33 | 34 | 35 | /// 36 | public SqlBuilder(IDbConnection connection, FormattableString value, InterpolatedSqlBuilderOptions? options = null) : base(value, options) 37 | { 38 | DbConnection = connection; 39 | } 40 | 41 | #if NET6_0_OR_GREATER 42 | /// 43 | public SqlBuilder(IDbConnection connection, int literalLength, int formattedCount, InterpolatedSqlBuilderOptions? options = null) : base(literalLength, formattedCount, options) 44 | { 45 | DbConnection = connection; 46 | } 47 | #endif 48 | #endregion 49 | 50 | #region Overrides 51 | /// 52 | /// Associated DbConnection 53 | /// 54 | public new IDbConnection DbConnection 55 | { 56 | get => base.DbConnection!; 57 | set => base.DbConnection = value; 58 | } 59 | #endregion 60 | 61 | #region Legacy compatibility (DapperQueryBuilder) 62 | public new string Sql => base.Sql; 63 | 64 | public SqlBuilderParameters Parameters => new SqlBuilderParameters(this); 65 | public class SqlBuilderParameters 66 | { 67 | public HashSet ParameterNames; 68 | protected ParametersDictionary _dapperParameters; 69 | public SqlBuilderParameters(SqlBuilder builder) 70 | { 71 | _dapperParameters = builder.Build().DapperParameters; 72 | ParameterNames = _dapperParameters.ParameterNames; 73 | } 74 | public T Get(string parameterName) 75 | { 76 | return _dapperParameters.Get(parameterName); 77 | } 78 | public SqlParameterInfo this[string parameterName] 79 | { 80 | get { return _dapperParameters[parameterName]; } 81 | } 82 | public int Count => _dapperParameters.Count; 83 | } 84 | #endregion 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlParameters/ParametersDictionary.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using InterpolatedSql.Dapper.SqlBuilders; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Linq; 7 | 8 | namespace InterpolatedSql.Dapper 9 | { 10 | /// 11 | /// List of SQL Parameters that are passed to Dapper methods 12 | /// 13 | public class ParametersDictionary : Dictionary, SqlMapper.IDynamicParameters, SqlMapper.IParameterCallbacks 14 | { 15 | #region Members 16 | private DynamicParameters? _dynamicParameters = null; 17 | #endregion 18 | 19 | #region ctors 20 | /// 21 | public ParametersDictionary() : base(StringComparer.OrdinalIgnoreCase) 22 | { 23 | } 24 | 25 | /// Creates a built from Implicit Parameters (loaded from ) 26 | /// and Explicit Parameters (loaded from ) 27 | public static ParametersDictionary LoadFrom(IInterpolatedSql sql, Func? calculateAutoParameterName = null) 28 | { 29 | sql = (sql as ISqlEnricher)?.GetEnrichedSql() ?? sql; 30 | var parameters = new ParametersDictionary(); 31 | //HashSet parmNames = new HashSet(StringComparer.OrdinalIgnoreCase); //TODO: check for name clashes, rename as required 32 | 33 | calculateAutoParameterName ??= ((sql as IDapperSqlBuilder)?.Options?.CalculateAutoParameterName ?? InterpolatedSql.SqlBuilders.InterpolatedSqlBuilderOptions.DefaultOptions.CalculateAutoParameterName); 34 | 35 | for (int i = 0; i < sql.ExplicitParameters.Count; i++) 36 | { 37 | parameters.Add(sql.ExplicitParameters[i]); 38 | } 39 | 40 | for (int i = 0; i < sql.SqlParameters.Count; i++) 41 | { 42 | // ParseArgument usually just returns parmValue (dbType and direction are only extracted if explicitly defined using format specifiers) 43 | // Dapper will pick the right DbType even if you don't explicitly specify the DbType - and for most cases size don't need to be specified 44 | 45 | var parmName = calculateAutoParameterName(sql.SqlParameters[i], i); 46 | var parmValue = sql.SqlParameters[i].Argument; 47 | var format = sql.SqlParameters[i].Format; 48 | 49 | if (parmValue is SqlParameterInfo parm) 50 | { 51 | parm.Name = parmName; 52 | parameters[parmName] = parm; 53 | } 54 | else 55 | parameters.Add(new SqlParameterInfo(parmName, parmValue)); 56 | } 57 | return parameters; 58 | } 59 | #endregion 60 | 61 | #region Methods 62 | /// 63 | /// Convert the current parameters into Dapper Parameters, since Dapper will automagically set DbTypes, Sizes, etc, and map to target database 64 | /// 65 | /// 66 | public virtual DynamicParameters DynamicParameters 67 | { 68 | // Most Dapper extensions work fine with a Dictionary{string, object}, 69 | // but some methods like QueryMultiple (when used with Stored Procedures) may require DynamicParameters 70 | // TODO: should we just use DynamicParameters in all Dapper calls? 71 | get 72 | { 73 | if (_dynamicParameters == null) 74 | { 75 | _dynamicParameters = new DynamicParameters(); 76 | foreach (var parameter in this.Values) 77 | SqlParameterMapper.Default.AddToDynamicParameters(_dynamicParameters, parameter); 78 | } 79 | return _dynamicParameters; 80 | } 81 | } 82 | 83 | /// 84 | /// Add a explicit parameter to this dictionary 85 | /// 86 | public void Add(SqlParameterInfo parameter) 87 | { 88 | this[parameter.Name!] = parameter; 89 | } 90 | 91 | 92 | /// 93 | /// Get parameter value 94 | /// 95 | public T Get(string key) => (T)this[key].Value; 96 | 97 | /// 98 | /// Parameter Names 99 | /// 100 | public HashSet ParameterNames => new HashSet(this.Keys); 101 | 102 | void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity) 103 | { 104 | // IDynamicParameters is explicitly implemented (not public) - and it will add our dynamic paramaters to IDbCommand 105 | ((SqlMapper.IDynamicParameters)DynamicParameters).AddParameters(command, identity); 106 | } 107 | 108 | /// 109 | /// After Dapper command is executed, we should get output/return parameters back 110 | /// 111 | void SqlMapper.IParameterCallbacks.OnCompleted() 112 | { 113 | var dapperParameters = DynamicParameters; 114 | 115 | // Update output and return parameters back 116 | foreach (var oparm in this.Values.Where(p => p.ParameterDirection != ParameterDirection.Input && p.ParameterDirection != null)) 117 | { 118 | oparm.Value = dapperParameters.Get(oparm.Name); 119 | oparm.OutputCallback?.Invoke(oparm.Value); 120 | } 121 | } 122 | #endregion 123 | 124 | /// 125 | /// Responsible for parsing SqlParameters (see ) 126 | /// into a list of SqlParameterInfo that 127 | /// 128 | public static SqlParameterMapper InterpolatedSqlParameterParser = new SqlParameterMapper(); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/DapperQueryBuilder/SqlParameters/SqlParameterMapper.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using InterpolatedSql.SqlBuilders; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Linq; 8 | 9 | namespace InterpolatedSql.Dapper 10 | { 11 | /// 12 | /// Maps from to Dapper Parameters. 13 | /// 14 | public class SqlParameterMapper 15 | { 16 | /// 17 | /// Calculates the name automatically assigned to interpolated parameters 18 | /// 19 | public virtual string CalculateAutoParameterName(InterpolatedSqlParameter parameter, int pos, InterpolatedSqlBuilderOptions options) 20 | { 21 | return options.AutoGeneratedParameterPrefix + 22 | (IsEnumerable(parameter.Argument) ? options.ParameterArrayNameSuffix : "") + 23 | pos.ToString(); 24 | } 25 | 26 | protected bool IsEnumerable(object? value) 27 | { 28 | if (value == null || value is DBNull) //SqlMapper.GetDbType 29 | return false; 30 | Type t = value.GetType(); 31 | return t != typeof(string) && typeof(IEnumerable).IsAssignableFrom(t); 32 | //TODO: use Dapper SqlMapper.LookupDbType ? 33 | } 34 | 35 | /// 36 | /// Converts from to Dapper Parameters. 37 | /// 38 | public virtual void AddToDynamicParameters(DynamicParameters target, SqlParameterInfo parameter) 39 | { 40 | //TODO: do implicit parameters have names here?! 41 | if (parameter is DbTypeParameterInfo dbParm) 42 | target.Add(parameter.Name, parameter.Value, dbParm.DbType, parameter.ParameterDirection ?? ParameterDirection.Input, dbParm.Size); 43 | else if (parameter is StringParameterInfo stringParm) 44 | target.Add(parameter.Name, new DbString() { Value = (string?)stringParm.Value, IsAnsi = stringParm.IsAnsi, IsFixedLength = stringParm.IsFixedLength, Length = stringParm.Length }); 45 | else if (parameter is SqlParameterInfo parm && parm.Value is IEnumerable stringParms) 46 | { 47 | target.Add(parameter.Name, stringParms.Select(stringParm => new DbString() { Value = (string?)stringParm.Value, IsAnsi = stringParm.IsAnsi, IsFixedLength = stringParm.IsFixedLength, Length = stringParm.Length })); 48 | } 49 | else 50 | target.Add(parameter.Name, parameter.Value); 51 | } 52 | 53 | /// 54 | /// Default mapper. By inheriting/overriding it's possible to modify this behavior 55 | /// 56 | public static SqlParameterMapper Default = new SqlParameterMapper(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/build.ps1: -------------------------------------------------------------------------------- 1 | $msbuild = ( 2 | "$Env:programfiles (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe", 3 | "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild.exe", 4 | "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\msbuild.exe", 5 | "$Env:programfiles (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\msbuild.exe", 6 | "$Env:programfiles\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\msbuild.exe", 7 | "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe", 8 | "${Env:ProgramFiles(x86)}\MSBuild\14.0\Bin\MSBuild.exe", 9 | "${Env:ProgramFiles(x86)}\MSBuild\12.0\Bin\MSBuild.exe" 10 | ) | Where-Object { Test-Path $_ } | Select-Object -first 1 11 | 12 | $configuration = 'Release' 13 | 14 | Remove-Item -Recurse -Force -ErrorAction Ignore ".\packages-local" 15 | Remove-Item -Recurse -Force -ErrorAction Ignore "$env:HOMEDRIVE$env:HOMEPATH\.nuget\packages\dapper-querybuilder" 16 | Remove-Item -Recurse -Force -ErrorAction Ignore "$env:HOMEDRIVE$env:HOMEPATH\.nuget\packages\dapperquerybuilder.strongname" 17 | 18 | New-Item -ItemType Directory -Force -Path ".\packages-local" 19 | 20 | dotnet clean DapperQueryBuilder.sln 21 | dotnet clean DapperQueryBuilder\DapperQueryBuilder.csproj 22 | dotnet clean DapperQueryBuilder.StrongName\DapperQueryBuilder.StrongName.csproj 23 | 24 | # DapperQueryBuilder + nupkg/snupkg (dotnet build is the best at restoring packages; but for deterministic builds we need msbuild) 25 | dotnet build -c release DapperQueryBuilder\DapperQueryBuilder.csproj 26 | & $msbuild "DapperQueryBuilder\DapperQueryBuilder.csproj" ` 27 | /t:Pack ` 28 | /p:PackageOutputPath="..\packages-local\" ` 29 | '/p:targetFrameworks="netstandard2.0;net462;net472;net5.0;net6.0;net7.0"' ` 30 | /p:Configuration=$configuration ` 31 | /p:IncludeSymbols=true ` 32 | /p:SymbolPackageFormat=snupkg ` 33 | /verbosity:minimal ` 34 | /p:ContinuousIntegrationBuild=true 35 | 36 | # DapperQueryBuilder.StrongName + nupkg/snupkg (dotnet build is the best at restoring packages; but for deterministic builds we need msbuild) 37 | dotnet build -c release DapperQueryBuilder.StrongName\DapperQueryBuilder.StrongName.csproj 38 | & $msbuild "DapperQueryBuilder.StrongName\DapperQueryBuilder.StrongName.csproj" ` 39 | /t:Pack ` 40 | /p:PackageOutputPath="..\packages-local\" ` 41 | '/p:targetFrameworks="netstandard2.0;net462;net472;net5.0;net6.0;net7.0"' ` 42 | /p:Configuration=$configuration ` 43 | /p:IncludeSymbols=true ` 44 | /p:SymbolPackageFormat=snupkg ` 45 | /verbosity:minimal ` 46 | /p:ContinuousIntegrationBuild=true 47 | 48 | 49 | 50 | 51 | # Unit tests 52 | if ($configuration -eq "Debug") 53 | { 54 | dotnet build -c release DapperQueryBuilder.Tests\DapperQueryBuilder.Tests.csproj 55 | dotnet test DapperQueryBuilder.Tests\DapperQueryBuilder.Tests.csproj 56 | } 57 | -------------------------------------------------------------------------------- /src/debug.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drizin/DapperQueryBuilder/e54e3af1a42d30e50443f97f8da6a129c39644bf/src/debug.snk --------------------------------------------------------------------------------