├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── Build.csproj ├── Dapper.sln ├── Dapper.snk ├── Directory.Build.props ├── Directory.Build.targets ├── License.txt ├── Readme.md ├── appveyor.yml ├── build.cmd ├── build.ps1 ├── docs ├── _config.yml └── index.md ├── nuget.config ├── src └── Dapper.Contrib │ ├── Dapper.Contrib.csproj │ ├── SqlMapperExtensions.Async.cs │ └── SqlMapperExtensions.cs ├── tests ├── Dapper.Tests.Contrib │ ├── Dapper.Tests.Contrib.csproj │ ├── Helpers │ │ ├── Attributes.cs │ │ └── XunitSkippable.cs │ ├── TestSuite.Async.cs │ ├── TestSuite.cs │ ├── TestSuites.cs │ └── xunit.runner.json ├── Directory.Build.props ├── Directory.Build.targets └── docker-compose.yml └── version.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome:http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Don't use tabs for indentation. 7 | [*] 8 | indent_style = space 9 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 10 | 11 | # Code files 12 | [*.{cs,csx,vb,vbx}] 13 | indent_size = 4 14 | insert_final_newline = true 15 | charset = utf-8-bom 16 | 17 | # Xml project files 18 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 19 | indent_size = 2 20 | 21 | # Xml config files 22 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 23 | indent_size = 2 24 | 25 | # JSON files 26 | [*.json] 27 | indent_size = 2 28 | 29 | # Dotnet code style settings: 30 | [*.{cs,vb}] 31 | # Sort using and Import directives with System.* appearing first 32 | dotnet_sort_system_directives_first = true 33 | # Avoid "this." and "Me." if not necessary 34 | dotnet_style_qualification_for_field = false:suggestion 35 | dotnet_style_qualification_for_property = false:suggestion 36 | dotnet_style_qualification_for_method = false:suggestion 37 | dotnet_style_qualification_for_event = false:suggestion 38 | 39 | # Use language keywords instead of framework type names for type references 40 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 41 | dotnet_style_predefined_type_for_member_access = true:suggestion 42 | 43 | # Suggest more modern language features when available 44 | dotnet_style_object_initializer = true:suggestion 45 | dotnet_style_collection_initializer = true:suggestion 46 | dotnet_style_coalesce_expression = true:suggestion 47 | dotnet_style_null_propagation = true:suggestion 48 | dotnet_style_explicit_tuple_names = true:suggestion 49 | 50 | # CSharp code style settings: 51 | [*.cs] 52 | # Prefer "var" everywhere 53 | #csharp_style_var_for_built_in_types = true:suggestion 54 | #csharp_style_var_when_type_is_apparent = false:suggestion 55 | #csharp_style_var_elsewhere = true:suggestion 56 | 57 | # Prefer method-like constructs to have a expression-body 58 | csharp_style_expression_bodied_methods = true:none 59 | csharp_style_expression_bodied_constructors = true:none 60 | csharp_style_expression_bodied_operators = true:none 61 | 62 | # Prefer property-like constructs to have an expression-body 63 | csharp_style_expression_bodied_properties = true:none 64 | csharp_style_expression_bodied_indexers = true:none 65 | csharp_style_expression_bodied_accessors = true:none 66 | 67 | # Suggest more modern language features when available 68 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 69 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 70 | csharp_style_inlined_variable_declaration = true:suggestion 71 | csharp_style_throw_expression = true:suggestion 72 | csharp_style_conditional_delegate_call = true:suggestion 73 | 74 | # Newline settings 75 | csharp_new_line_before_open_brace = all 76 | csharp_new_line_before_else = true 77 | csharp_new_line_before_catch = true 78 | csharp_new_line_before_finally = true 79 | csharp_new_line_before_members_in_object_initializers = true 80 | csharp_new_line_before_members_in_anonymous_types = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.doc diff=astextplain 4 | *.DOC diff=astextplain 5 | *.docx diff=astextplain 6 | *.DOCX diff=astextplain 7 | *.dot diff=astextplain 8 | *.DOT diff=astextplain 9 | *.pdf diff=astextplain 10 | *.PDF diff=astextplain 11 | *.rtf diff=astextplain 12 | *.RTF diff=astextplain 13 | 14 | *.jpg binary 15 | *.png binary 16 | *.gif binary 17 | 18 | *.cs -text diff=csharp 19 | *.vb -text 20 | *.c -text 21 | *.cpp -text 22 | *.cxx -text 23 | *.h -text 24 | *.hxx -text 25 | *.py -text 26 | *.rb -text 27 | *.java -text 28 | *.html -text 29 | *.htm -text 30 | *.css -text 31 | *.scss -text 32 | *.sass -text 33 | *.less -text 34 | *.js -text 35 | *.lisp -text 36 | *.clj -text 37 | *.sql -text 38 | *.php -text 39 | *.lua -text 40 | *.m -text 41 | *.asm -text 42 | *.erl -text 43 | *.fs -text 44 | *.fsx -text 45 | *.hs -text 46 | 47 | *.csproj -text merge=union 48 | *.vbproj -text merge=union 49 | *.fsproj -text merge=union 50 | *.dbproj -text merge=union 51 | *.sln -text merge=union 52 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - '*' 10 | - '!/docs/*' # Don't run workflow when files are only in the /docs directory 11 | 12 | jobs: 13 | vm-job: 14 | name: Ubuntu 15 | runs-on: ubuntu-latest 16 | services: 17 | postgres: 18 | image: postgres 19 | ports: 20 | - 5432/tcp 21 | env: 22 | POSTGRES_USER: postgres 23 | POSTGRES_PASSWORD: postgres 24 | POSTGRES_DB: test 25 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 26 | sqlserver: 27 | image: mcr.microsoft.com/mssql/server:2019-latest 28 | ports: 29 | - 1433/tcp 30 | env: 31 | ACCEPT_EULA: Y 32 | SA_PASSWORD: "Password." 33 | mysql: 34 | image: mysql 35 | ports: 36 | - 3306/tcp 37 | env: 38 | MYSQL_ROOT_PASSWORD: root 39 | MYSQL_DATABASE: test 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v1 43 | - name: .NET Build 44 | run: dotnet build Build.csproj -c Release /p:CI=true 45 | - name: Dapper.Contrib Tests 46 | run: dotnet test tests/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj -c Release --logger GitHubActions /p:CI=true 47 | env: 48 | MySqlConnectionString: Server=localhost;Port=${{ job.services.mysql.ports[3306] }};Uid=root;Pwd=root;Database=test;Allow User Variables=true 49 | OLEDBConnectionString: Provider=SQLOLEDB;Server=tcp:localhost,${{ job.services.sqlserver.ports[1433] }};Database=tempdb;User Id=sa;Password=Password.; 50 | PostgesConnectionString: Server=localhost;Port=${{ job.services.postgres.ports[5432] }};Database=test;User Id=postgres;Password=postgres; 51 | SqlServerConnectionString: Server=tcp:localhost,${{ job.services.sqlserver.ports[1433] }};Database=tempdb;User Id=sa;Password=Password.; 52 | - name: .NET Lib Pack 53 | run: dotnet pack Build.csproj --no-build -c Release /p:PackageOutputPath=%CD%\.nupkgs /p:CI=true 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.suo 2 | .vs/ 3 | .vscode/ 4 | bin/ 5 | obj/ 6 | /*.user 7 | _Resharper* 8 | .hgtags 9 | NuGet.exe 10 | *.user 11 | *.nupkg 12 | .nupkgs/ 13 | .docstats 14 | *.ide/ 15 | *.lock.json 16 | *.coverage 17 | Test.DB.* 18 | TestResults/ 19 | .dotnet/* 20 | BenchmarkDotNet.Artifacts/ 21 | .idea/ 22 | .DS_Store -------------------------------------------------------------------------------- /Build.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dapper.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28917.182 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A34907DF-958A-4E4C-8491-84CF303FD13E}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | appveyor.yml = appveyor.yml 10 | build.ps1 = build.ps1 11 | Directory.Build.props = Directory.Build.props 12 | docs\index.md = docs\index.md 13 | License.txt = License.txt 14 | nuget.config = nuget.config 15 | Readme.md = Readme.md 16 | version.json = version.json 17 | EndProjectSection 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Contrib", "Dapper.Contrib\Dapper.Contrib.csproj", "{4E409F8F-CFBB-4332-8B0A-FD5A283051FD}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapper.Tests.Contrib", "tests\Dapper.Tests.Contrib\Dapper.Tests.Contrib.csproj", "{DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E956F6B-6BD8-46F5-BC85-49292FF8F9AB}" 24 | ProjectSection(SolutionItems) = preProject 25 | Directory.Build.props = Directory.Build.props 26 | EndProjectSection 27 | EndProject 28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{568BD46C-1C65-4D44-870C-12CD72563262}" 29 | ProjectSection(SolutionItems) = preProject 30 | tests\Directory.Build.props = tests\Directory.Build.props 31 | tests\Directory.Build.targets = tests\Directory.Build.targets 32 | tests\docker-compose.yml = tests\docker-compose.yml 33 | EndProjectSection 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {4E409F8F-CFBB-4332-8B0A-FD5A283051FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {4E409F8F-CFBB-4332-8B0A-FD5A283051FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {4E409F8F-CFBB-4332-8B0A-FD5A283051FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {4E409F8F-CFBB-4332-8B0A-FD5A283051FD}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A}.Release|Any CPU.Build.0 = Release|Any CPU 49 | EndGlobalSection 50 | GlobalSection(SolutionProperties) = preSolution 51 | HideSolutionNode = FALSE 52 | EndGlobalSection 53 | GlobalSection(NestedProjects) = preSolution 54 | {4E409F8F-CFBB-4332-8B0A-FD5A283051FD} = {4E956F6B-6BD8-46F5-BC85-49292FF8F9AB} 55 | {DAB3C5B7-BCD1-4A5F-BB6B-50D2BB63DB4A} = {568BD46C-1C65-4D44-870C-12CD72563262} 56 | EndGlobalSection 57 | GlobalSection(ExtensibilityGlobals) = postSolution 58 | SolutionGuid = {928A4226-96F3-409A-8A83-9E7444488710} 59 | EndGlobalSection 60 | EndGlobal 61 | -------------------------------------------------------------------------------- /Dapper.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DapperLib/Dapper.Contrib/cf24f6bdc577b1e071c3764ddfb2cf3382531405/Dapper.snk -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2019 Stack Exchange, Inc. 4 | 5 | true 6 | true 7 | ../Dapper.snk 8 | 9 | $(AssemblyName) 10 | https://dapperlib.github.io/Dapper.Contrib/ 11 | https://github.com/DapperLib/Dapper.Contrib 12 | Apache-2.0 13 | Dapper.png 14 | git 15 | https://github.com/DapperLib/Dapper.Contrib 16 | false 17 | $(NOWARN);IDE0056;IDE0057;IDE0079 18 | true 19 | embedded 20 | en-US 21 | false 22 | 23 | true 24 | 25 | 9.0 26 | 27 | 28 | 29 | true 30 | true 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The Dapper.Contrib library and tools are licenced under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Dapper.Contrib - a simple object mapper for .Net 2 | ======================================== 3 | [![Build status](https://ci.appveyor.com/api/projects/status/1w448i6nfxd14w75?svg=true)](https://ci.appveyor.com/project/StackExchange/dapper-contrib) 4 | 5 | Release Notes 6 | ------------- 7 | Located at [dapperlib.github.io/Dapper.Contrib](https://dapperlib.github.io/Dapper.Contrib/) 8 | 9 | Packages 10 | -------- 11 | 12 | MyGet Pre-release feed: https://www.myget.org/gallery/dapper 13 | 14 | | Package | NuGet Stable | NuGet Pre-release | Downloads | MyGet | 15 | | ------- | ------------ | ----------------- | --------- | ----- | 16 | | [Dapper.Contrib](https://www.nuget.org/packages/Dapper.Contrib/) | [![Dapper.Contrib](https://img.shields.io/nuget/v/Dapper.Contrib.svg)](https://www.nuget.org/packages/Dapper.Contrib/) | [![Dapper.Contrib](https://img.shields.io/nuget/vpre/Dapper.Contrib.svg)](https://www.nuget.org/packages/Dapper.Contrib/) | [![Dapper.Contrib](https://img.shields.io/nuget/dt/Dapper.Contrib.svg)](https://www.nuget.org/packages/Dapper.Contrib/) | [![Dapper.Contrib MyGet](https://img.shields.io/myget/dapper/vpre/Dapper.Contrib.svg)](https://www.myget.org/feed/dapper/package/nuget/Dapper.Contrib) | 17 | 18 | Features 19 | -------- 20 | 21 | Dapper.Contrib contains a number of helper methods for inserting, getting, 22 | updating and deleting records. 23 | 24 | The full list of extension methods in Dapper.Contrib right now are: 25 | 26 | ```csharp 27 | T Get(id); 28 | IEnumerable GetAll(); 29 | int Insert(T obj); 30 | int Insert(Enumerable list); 31 | bool Update(T obj); 32 | bool Update(Enumerable list); 33 | bool Delete(T obj); 34 | bool Delete(Enumerable list); 35 | bool DeleteAll(); 36 | ``` 37 | 38 | For these extensions to work, the entity in question _MUST_ have a 39 | key property. Dapper will automatically use a property named "`id`" 40 | (case-insensitive) as the key property, if one is present. 41 | 42 | ```csharp 43 | public class Car 44 | { 45 | public int Id { get; set; } // Works by convention 46 | public string Name { get; set; } 47 | } 48 | ``` 49 | 50 | If the entity doesn't follow this convention, decorate 51 | a specific property with a `[Key]` or `[ExplicitKey]` attribute. 52 | 53 | ```csharp 54 | public class User 55 | { 56 | [Key] 57 | int TheId { get; set; } 58 | string Name { get; set; } 59 | int Age { get; set; } 60 | } 61 | ``` 62 | 63 | `[Key]` should be used for database-generated keys (e.g. autoincrement columns), 64 | while `[ExplicitKey]` should be used for explicit keys generated in code. 65 | 66 | `Get` methods 67 | ------- 68 | 69 | Get one specific entity based on id 70 | 71 | ```csharp 72 | var car = connection.Get(1); 73 | ``` 74 | 75 | or a list of all entities in the table. 76 | 77 | ```csharp 78 | var cars = connection.GetAll(); 79 | ``` 80 | 81 | `Insert` methods 82 | ------- 83 | 84 | Insert one entity 85 | 86 | ```csharp 87 | connection.Insert(new Car { Name = "Volvo" }); 88 | ``` 89 | 90 | or a list of entities. 91 | 92 | ```csharp 93 | connection.Insert(cars); 94 | ``` 95 | 96 | 97 | 98 | `Update` methods 99 | ------- 100 | Update one specific entity 101 | 102 | ```csharp 103 | connection.Update(new Car() { Id = 1, Name = "Saab" }); 104 | ``` 105 | 106 | or update a list of entities. 107 | 108 | ```csharp 109 | connection.Update(cars); 110 | ``` 111 | 112 | `Delete` methods 113 | ------- 114 | Delete an entity by the specified `[Key]` property 115 | 116 | ```csharp 117 | connection.Delete(new Car() { Id = 1 }); 118 | ``` 119 | 120 | a list of entities 121 | 122 | ```csharp 123 | connection.Delete(cars); 124 | ``` 125 | 126 | or _ALL_ entities in the table. 127 | 128 | ```csharp 129 | connection.DeleteAll(); 130 | ``` 131 | 132 | Special Attributes 133 | ---------- 134 | Dapper.Contrib makes use of some optional attributes: 135 | 136 | * `[Table("Tablename")]` - use another table name instead of the (by default pluralized) name of the class 137 | 138 | ```csharp 139 | [Table ("emps")] 140 | public class Employee 141 | { 142 | public int Id { get; set; } 143 | public string Name { get; set; } 144 | } 145 | ``` 146 | * `[Key]` - this property represents a database-generated identity/key 147 | 148 | ```csharp 149 | public class Employee 150 | { 151 | [Key] 152 | public int EmployeeId { get; set; } 153 | public string Name { get; set; } 154 | } 155 | ``` 156 | * `[ExplicitKey]` - this property represents an explicit identity/key which is 157 | *not* automatically generated by the database 158 | 159 | ```csharp 160 | public class Employee 161 | { 162 | [ExplicitKey] 163 | public Guid EmployeeId { get; set; } 164 | public string Name { get; set; } 165 | } 166 | ``` 167 | * `[Write(true/false)]` - this property is (not) writeable 168 | * `[Computed]` - this property is computed and should not be part of updates 169 | 170 | Limitations and caveats 171 | ------- 172 | 173 | ### SQLite 174 | 175 | `SQLiteConnection` exposes an `Update` event that clashes with the `Update` 176 | extension provided by Dapper.Contrib. There are 2 ways to deal with this. 177 | 178 | 1. Call the `Update` method explicitly from `SqlMapperExtensions` 179 | 180 | ```Csharp 181 | SqlMapperExtensions.Update(_conn, new Employee { Id = 1, Name = "Mercedes" }); 182 | ``` 183 | 2. Make the method signature unique by passing a type parameter to `Update` 184 | 185 | ```Csharp 186 | connection.Update(new Car() { Id = 1, Name = "Maruti" }); 187 | ``` 188 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2019 2 | 3 | skip_branch_with_pr: true 4 | skip_tags: true 5 | skip_commits: 6 | files: 7 | - '**/*.md' 8 | 9 | environment: 10 | Appveyor: true 11 | # Postgres 12 | POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 13 | PGUSER: postgres 14 | PGPASSWORD: Password12! 15 | POSTGRES_ENV_POSTGRES_USER: postgres 16 | POSTGRES_ENV_POSTGRES_PASSWORD: Password12! 17 | POSTGRES_ENV_POSTGRES_DB: test 18 | # MySQL 19 | MYSQL_PATH: C:\Program Files\MySql\MySQL Server 5.7 20 | MYSQL_PWD: Password12! 21 | MYSQL_ENV_MYSQL_USER: root 22 | MYSQL_ENV_MYSQL_PASSWORD: Password12! 23 | MYSQL_ENV_MYSQL_DATABASE: test 24 | # Connection strings for tests: 25 | MySqlConnectionString: Server=localhost;Database=test;Uid=root;Pwd=Password12! 26 | OLEDBConnectionString: Provider=SQLOLEDB;Data Source=(local)\SQL2019;Initial Catalog=tempdb;User Id=sa;Password=Password12! 27 | PostgesConnectionString: Server=localhost;Port=5432;User Id=postgres;Password=Password12!;Database=test 28 | SqlServerConnectionString: Server=(local)\SQL2019;Database=tempdb;User ID=sa;Password=Password12! 29 | 30 | services: 31 | - mysql 32 | - postgresql 33 | 34 | init: 35 | - git config --global core.autocrlf input 36 | - SET PATH=%POSTGRES_PATH%\bin;%MYSQL_PATH%\bin;%PATH% 37 | - net start MSSQL$SQL2019 38 | 39 | nuget: 40 | disable_publish_on_pr: true 41 | 42 | build_script: 43 | # Postgres 44 | - createdb test 45 | # MySQL 46 | - mysql -e "create database test;" --user=root 47 | # Our stuff 48 | - ps: .\build.ps1 -PullRequestNumber "$env:APPVEYOR_PULL_REQUEST_NUMBER" -CreatePackages $true 49 | 50 | test: off 51 | artifacts: 52 | - path: .\.nupkgs\*.nupkg 53 | 54 | deploy: 55 | - provider: NuGet 56 | server: https://www.myget.org/F/stackoverflow/api/v2 57 | on: 58 | branch: main 59 | api_key: 60 | secure: P/UHxq2DEs0GI1SoDXDesHjRVsSVgdywz5vmsnhFQQY5aJgO3kP+QfhwfhXz19Rw 61 | symbol_server: https://www.myget.org/F/stackoverflow/symbols/api/v2/package 62 | - provider: NuGet 63 | server: https://www.myget.org/F/dapper/api/v2 64 | on: 65 | branch: main 66 | api_key: 67 | secure: PV7ERAltWWLhy7AT2h+Vb5c1BM9/WFgvggb+rKyQ8hDg3fYqpZauYdidOOgt2lp4 68 | symbol_server: https://www.myget.org/F/dapper/api/v2/package -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0build.ps1' %*; exit $LASTEXITCODE" -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(PositionalBinding=$false)] 2 | param( 3 | [bool] $CreatePackages, 4 | [bool] $RunTests = $true, 5 | [string] $PullRequestNumber 6 | ) 7 | 8 | Write-Host "Run Parameters:" -ForegroundColor Cyan 9 | Write-Host " CreatePackages: $CreatePackages" 10 | Write-Host " RunTests: $RunTests" 11 | Write-Host " dotnet --version:" (dotnet --version) 12 | 13 | $packageOutputFolder = "$PSScriptRoot\.nupkgs" 14 | 15 | if ($PullRequestNumber) { 16 | Write-Host "Building for a pull request (#$PullRequestNumber), skipping packaging." -ForegroundColor Yellow 17 | $CreatePackages = $false 18 | } 19 | 20 | Write-Host "Building all projects (Build.csproj traversal)..." -ForegroundColor "Magenta" 21 | dotnet build ".\Build.csproj" -c Release /p:CI=true 22 | Write-Host "Done building." -ForegroundColor "Green" 23 | 24 | if ($RunTests) { 25 | Write-Host "Running tests: Build.csproj traversal (all frameworks)" -ForegroundColor "Magenta" 26 | dotnet test ".\Build.csproj" -c Release --no-build 27 | if ($LastExitCode -ne 0) { 28 | Write-Host "Error with tests, aborting build." -Foreground "Red" 29 | Exit 1 30 | } 31 | Write-Host "Tests passed!" -ForegroundColor "Green" 32 | } 33 | 34 | if ($CreatePackages) { 35 | New-Item -ItemType Directory -Path $packageOutputFolder -Force | Out-Null 36 | Write-Host "Clearing existing $packageOutputFolder..." -NoNewline 37 | Get-ChildItem $packageOutputFolder | Remove-Item 38 | Write-Host "done." -ForegroundColor "Green" 39 | 40 | Write-Host "Building all packages" -ForegroundColor "Green" 41 | dotnet pack ".\Build.csproj" --no-build -c Release /p:PackageOutputPath=$packageOutputFolder /p:CI=true 42 | } 43 | Write-Host "Build Complete." -ForegroundColor "Green" 44 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Dapper.Contrib - Extensions for Dapper 2 | 3 | ## Overview 4 | 5 | A brief guide is available [on github](https://github.com/DapperLib/Dapper.Contrib/blob/main/Readme.md) 6 | 7 | ## Installation 8 | 9 | From NuGet: 10 | 11 | Install-Package Dapper.Contrib 12 | 13 | Note: to get the latest pre-release build, add ` -Pre` to the end of the command. 14 | 15 | ## Release Notes 16 | 17 | ### Unreleased 18 | 19 | (note: new PRs will not be merged until they add release note wording here) 20 | 21 | ### Previous Releases 22 | 23 | Prior to v2.0.90, Dapper.Contrib was part of the main repository - please see release notes at [https://dapperlib.github.io/Dapper/](https://dapperlib.github.io/Dapper/) 24 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Dapper.Contrib/Dapper.Contrib.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Dapper.Contrib 4 | Dapper.Contrib 5 | orm;sql;micro-orm;dapper 6 | The official collection of get, insert, update and delete helpers for Dapper.net. Also handles lists of entities and optional "dirty" tracking of interface-based entities. 7 | Sam Saffron;Johan Danforth 8 | net461;netstandard2.0;net5.0 9 | false 10 | $(NoWarn);CA1050 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Dapper.Contrib/SqlMapperExtensions.Async.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Dapper; 9 | 10 | namespace Dapper.Contrib.Extensions 11 | { 12 | public static partial class SqlMapperExtensions 13 | { 14 | /// 15 | /// Returns a single entity by a single id from table "Ts" asynchronously using Task. T must be of interface type. 16 | /// Id must be marked with [Key] attribute. 17 | /// Created entity is tracked/intercepted for changes and used by the Update() extension. 18 | /// 19 | /// Interface type to create and populate 20 | /// Open SqlConnection 21 | /// Id of the entity to get, must be marked with [Key] attribute 22 | /// The transaction to run under, null (the default) if none 23 | /// Number of seconds before command execution timeout 24 | /// Entity of T 25 | public static async Task GetAsync(this IDbConnection connection, dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 26 | { 27 | var type = typeof(T); 28 | if (!GetQueries.TryGetValue(type.TypeHandle, out string sql)) 29 | { 30 | var key = GetSingleKey(nameof(GetAsync)); 31 | var name = GetTableName(type); 32 | 33 | sql = $"SELECT * FROM {name} WHERE {key.Name} = @id"; 34 | GetQueries[type.TypeHandle] = sql; 35 | } 36 | 37 | var dynParams = new DynamicParameters(); 38 | dynParams.Add("@id", id); 39 | 40 | if (!type.IsInterface) 41 | return (await connection.QueryAsync(sql, dynParams, transaction, commandTimeout).ConfigureAwait(false)).FirstOrDefault(); 42 | 43 | if (!((await connection.QueryAsync(sql, dynParams).ConfigureAwait(false)).FirstOrDefault() is IDictionary res)) 44 | { 45 | return null; 46 | } 47 | 48 | var obj = ProxyGenerator.GetInterfaceProxy(); 49 | 50 | foreach (var property in TypePropertiesCache(type)) 51 | { 52 | var val = res[property.Name]; 53 | if (val == null) continue; 54 | if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) 55 | { 56 | var genericType = Nullable.GetUnderlyingType(property.PropertyType); 57 | if (genericType != null) property.SetValue(obj, Convert.ChangeType(val, genericType), null); 58 | } 59 | else 60 | { 61 | property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); 62 | } 63 | } 64 | 65 | ((IProxy)obj).IsDirty = false; //reset change tracking and return 66 | 67 | return obj; 68 | } 69 | 70 | /// 71 | /// Returns a list of entities from table "Ts". 72 | /// Id of T must be marked with [Key] attribute. 73 | /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension 74 | /// for optimal performance. 75 | /// 76 | /// Interface or type to create and populate 77 | /// Open SqlConnection 78 | /// The transaction to run under, null (the default) if none 79 | /// Number of seconds before command execution timeout 80 | /// Entity of T 81 | public static Task> GetAllAsync(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 82 | { 83 | var type = typeof(T); 84 | var cacheType = typeof(List); 85 | 86 | if (!GetQueries.TryGetValue(cacheType.TypeHandle, out string sql)) 87 | { 88 | GetSingleKey(nameof(GetAll)); 89 | var name = GetTableName(type); 90 | 91 | sql = "SELECT * FROM " + name; 92 | GetQueries[cacheType.TypeHandle] = sql; 93 | } 94 | 95 | if (!type.IsInterface) 96 | { 97 | return connection.QueryAsync(sql, null, transaction, commandTimeout); 98 | } 99 | return GetAllAsyncImpl(connection, transaction, commandTimeout, sql, type); 100 | } 101 | 102 | private static async Task> GetAllAsyncImpl(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string sql, Type type) where T : class 103 | { 104 | var result = await connection.QueryAsync(sql, transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); 105 | var list = new List(); 106 | foreach (IDictionary res in result) 107 | { 108 | var obj = ProxyGenerator.GetInterfaceProxy(); 109 | foreach (var property in TypePropertiesCache(type)) 110 | { 111 | var val = res[property.Name]; 112 | if (val == null) continue; 113 | if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) 114 | { 115 | var genericType = Nullable.GetUnderlyingType(property.PropertyType); 116 | if (genericType != null) property.SetValue(obj, Convert.ChangeType(val, genericType), null); 117 | } 118 | else 119 | { 120 | property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); 121 | } 122 | } 123 | ((IProxy)obj).IsDirty = false; //reset change tracking and return 124 | list.Add(obj); 125 | } 126 | return list; 127 | } 128 | 129 | /// 130 | /// Inserts an entity into table "Ts" asynchronously using Task and returns identity id. 131 | /// 132 | /// The type being inserted. 133 | /// Open SqlConnection 134 | /// Entity to insert 135 | /// The transaction to run under, null (the default) if none 136 | /// Number of seconds before command execution timeout 137 | /// The specific ISqlAdapter to use, auto-detected based on connection if null 138 | /// Identity of inserted entity 139 | public static Task InsertAsync(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, 140 | int? commandTimeout = null, ISqlAdapter sqlAdapter = null) where T : class 141 | { 142 | var type = typeof(T); 143 | sqlAdapter ??= GetFormatter(connection); 144 | 145 | var isList = false; 146 | if (type.IsArray) 147 | { 148 | isList = true; 149 | type = type.GetElementType(); 150 | } 151 | else if (type.IsGenericType) 152 | { 153 | var typeInfo = type.GetTypeInfo(); 154 | bool implementsGenericIEnumerableOrIsGenericIEnumerable = 155 | typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || 156 | typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); 157 | 158 | if (implementsGenericIEnumerableOrIsGenericIEnumerable) 159 | { 160 | isList = true; 161 | type = type.GetGenericArguments()[0]; 162 | } 163 | } 164 | 165 | var name = GetTableName(type); 166 | var sbColumnList = new StringBuilder(null); 167 | var allProperties = TypePropertiesCache(type); 168 | var keyProperties = KeyPropertiesCache(type).ToList(); 169 | var computedProperties = ComputedPropertiesCache(type); 170 | var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); 171 | 172 | for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) 173 | { 174 | var property = allPropertiesExceptKeyAndComputed[i]; 175 | sqlAdapter.AppendColumnName(sbColumnList, property.Name); 176 | if (i < allPropertiesExceptKeyAndComputed.Count - 1) 177 | sbColumnList.Append(", "); 178 | } 179 | 180 | var sbParameterList = new StringBuilder(null); 181 | for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) 182 | { 183 | var property = allPropertiesExceptKeyAndComputed[i]; 184 | sbParameterList.AppendFormat("@{0}", property.Name); 185 | if (i < allPropertiesExceptKeyAndComputed.Count - 1) 186 | sbParameterList.Append(", "); 187 | } 188 | 189 | if (!isList) //single entity 190 | { 191 | return sqlAdapter.InsertAsync(connection, transaction, commandTimeout, name, sbColumnList.ToString(), 192 | sbParameterList.ToString(), keyProperties, entityToInsert); 193 | } 194 | 195 | //insert list of entities 196 | var cmd = $"INSERT INTO {name} ({sbColumnList}) values ({sbParameterList})"; 197 | return connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout); 198 | } 199 | 200 | /// 201 | /// Updates entity in table "Ts" asynchronously using Task, checks if the entity is modified if the entity is tracked by the Get() extension. 202 | /// 203 | /// Type to be updated 204 | /// Open SqlConnection 205 | /// Entity to be updated 206 | /// The transaction to run under, null (the default) if none 207 | /// Number of seconds before command execution timeout 208 | /// true if updated, false if not found or not modified (tracked entities) 209 | public static async Task UpdateAsync(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 210 | { 211 | if ((entityToUpdate is IProxy proxy) && !proxy.IsDirty) 212 | { 213 | return false; 214 | } 215 | 216 | var type = typeof(T); 217 | 218 | if (type.IsArray) 219 | { 220 | type = type.GetElementType(); 221 | } 222 | else if (type.IsGenericType) 223 | { 224 | var typeInfo = type.GetTypeInfo(); 225 | bool implementsGenericIEnumerableOrIsGenericIEnumerable = 226 | typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || 227 | typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); 228 | 229 | if (implementsGenericIEnumerableOrIsGenericIEnumerable) 230 | { 231 | type = type.GetGenericArguments()[0]; 232 | } 233 | } 234 | 235 | var keyProperties = KeyPropertiesCache(type).ToList(); 236 | var explicitKeyProperties = ExplicitKeyPropertiesCache(type); 237 | if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) 238 | throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); 239 | 240 | var name = GetTableName(type); 241 | 242 | var sb = new StringBuilder(); 243 | sb.AppendFormat("update {0} set ", name); 244 | 245 | var allProperties = TypePropertiesCache(type); 246 | keyProperties.AddRange(explicitKeyProperties); 247 | var computedProperties = ComputedPropertiesCache(type); 248 | var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); 249 | 250 | var adapter = GetFormatter(connection); 251 | 252 | for (var i = 0; i < nonIdProps.Count; i++) 253 | { 254 | var property = nonIdProps[i]; 255 | adapter.AppendColumnNameEqualsValue(sb, property.Name); 256 | if (i < nonIdProps.Count - 1) 257 | sb.Append(", "); 258 | } 259 | sb.Append(" where "); 260 | for (var i = 0; i < keyProperties.Count; i++) 261 | { 262 | var property = keyProperties[i]; 263 | adapter.AppendColumnNameEqualsValue(sb, property.Name); 264 | if (i < keyProperties.Count - 1) 265 | sb.Append(" and "); 266 | } 267 | var updated = await connection.ExecuteAsync(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction).ConfigureAwait(false); 268 | return updated > 0; 269 | } 270 | 271 | /// 272 | /// Delete entity in table "Ts" asynchronously using Task. 273 | /// 274 | /// Type of entity 275 | /// Open SqlConnection 276 | /// Entity to delete 277 | /// The transaction to run under, null (the default) if none 278 | /// Number of seconds before command execution timeout 279 | /// true if deleted, false if not found 280 | public static async Task DeleteAsync(this IDbConnection connection, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 281 | { 282 | if (entityToDelete == null) 283 | throw new ArgumentException("Cannot Delete null Object", nameof(entityToDelete)); 284 | 285 | var type = typeof(T); 286 | 287 | if (type.IsArray) 288 | { 289 | type = type.GetElementType(); 290 | } 291 | else if (type.IsGenericType) 292 | { 293 | var typeInfo = type.GetTypeInfo(); 294 | bool implementsGenericIEnumerableOrIsGenericIEnumerable = 295 | typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || 296 | typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); 297 | 298 | if (implementsGenericIEnumerableOrIsGenericIEnumerable) 299 | { 300 | type = type.GetGenericArguments()[0]; 301 | } 302 | } 303 | 304 | var keyProperties = KeyPropertiesCache(type); 305 | var explicitKeyProperties = ExplicitKeyPropertiesCache(type); 306 | if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) 307 | throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); 308 | 309 | var name = GetTableName(type); 310 | var allKeyProperties = keyProperties.Concat(explicitKeyProperties).ToList(); 311 | 312 | var sb = new StringBuilder(); 313 | sb.AppendFormat("DELETE FROM {0} WHERE ", name); 314 | 315 | var adapter = GetFormatter(connection); 316 | 317 | for (var i = 0; i < allKeyProperties.Count; i++) 318 | { 319 | var property = allKeyProperties[i]; 320 | adapter.AppendColumnNameEqualsValue(sb, property.Name); 321 | if (i < allKeyProperties.Count - 1) 322 | sb.Append(" AND "); 323 | } 324 | var deleted = await connection.ExecuteAsync(sb.ToString(), entityToDelete, transaction, commandTimeout).ConfigureAwait(false); 325 | return deleted > 0; 326 | } 327 | 328 | /// 329 | /// Delete all entities in the table related to the type T asynchronously using Task. 330 | /// 331 | /// Type of entity 332 | /// Open SqlConnection 333 | /// The transaction to run under, null (the default) if none 334 | /// Number of seconds before command execution timeout 335 | /// true if deleted, false if none found 336 | public static async Task DeleteAllAsync(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 337 | { 338 | var type = typeof(T); 339 | var statement = "DELETE FROM " + GetTableName(type); 340 | var deleted = await connection.ExecuteAsync(statement, null, transaction, commandTimeout).ConfigureAwait(false); 341 | return deleted > 0; 342 | } 343 | } 344 | } 345 | 346 | public partial interface ISqlAdapter 347 | { 348 | /// 349 | /// Inserts into the database, returning the Id of the row created. 350 | /// 351 | /// The connection to use. 352 | /// The transaction to use. 353 | /// The command timeout to use. 354 | /// The table to insert into. 355 | /// The columns to set with this insert. 356 | /// The parameters to set for this insert. 357 | /// The key columns in this table. 358 | /// The entity to insert. 359 | /// The Id of the row created. 360 | Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert); 361 | } 362 | 363 | public partial class SqlServerAdapter 364 | { 365 | /// 366 | /// Inserts into the database, returning the Id of the row created. 367 | /// 368 | /// The connection to use. 369 | /// The transaction to use. 370 | /// The command timeout to use. 371 | /// The table to insert into. 372 | /// The columns to set with this insert. 373 | /// The parameters to set for this insert. 374 | /// The key columns in this table. 375 | /// The entity to insert. 376 | /// The Id of the row created. 377 | public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 378 | { 379 | var cmd = $"INSERT INTO {tableName} ({columnList}) values ({parameterList}); SELECT SCOPE_IDENTITY() id"; 380 | var multi = await connection.QueryMultipleAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); 381 | 382 | var first = await multi.ReadFirstOrDefaultAsync().ConfigureAwait(false); 383 | if (first == null || first.id == null) return 0; 384 | 385 | var id = (int)first.id; 386 | var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 387 | if (pi.Length == 0) return id; 388 | 389 | var idp = pi[0]; 390 | idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); 391 | 392 | return id; 393 | } 394 | } 395 | 396 | public partial class SqlCeServerAdapter 397 | { 398 | /// 399 | /// Inserts into the database, returning the Id of the row created. 400 | /// 401 | /// The connection to use. 402 | /// The transaction to use. 403 | /// The command timeout to use. 404 | /// The table to insert into. 405 | /// The columns to set with this insert. 406 | /// The parameters to set for this insert. 407 | /// The key columns in this table. 408 | /// The entity to insert. 409 | /// The Id of the row created. 410 | public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 411 | { 412 | var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList})"; 413 | await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); 414 | var r = (await connection.QueryAsync("SELECT @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false)).ToList(); 415 | 416 | if (r[0] == null || r[0].id == null) return 0; 417 | var id = (int)r[0].id; 418 | 419 | var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 420 | if (pi.Length == 0) return id; 421 | 422 | var idp = pi[0]; 423 | idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); 424 | 425 | return id; 426 | } 427 | } 428 | 429 | public partial class MySqlAdapter 430 | { 431 | /// 432 | /// Inserts into the database, returning the Id of the row created. 433 | /// 434 | /// The connection to use. 435 | /// The transaction to use. 436 | /// The command timeout to use. 437 | /// The table to insert into. 438 | /// The columns to set with this insert. 439 | /// The parameters to set for this insert. 440 | /// The key columns in this table. 441 | /// The entity to insert. 442 | /// The Id of the row created. 443 | public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, 444 | string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 445 | { 446 | var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList})"; 447 | await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); 448 | var r = await connection.QueryAsync("SELECT LAST_INSERT_ID() id", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); 449 | 450 | var id = r.First().id; 451 | if (id == null) return 0; 452 | var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 453 | if (pi.Length == 0) return Convert.ToInt32(id); 454 | 455 | var idp = pi[0]; 456 | idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); 457 | 458 | return Convert.ToInt32(id); 459 | } 460 | } 461 | 462 | public partial class PostgresAdapter 463 | { 464 | /// 465 | /// Inserts into the database, returning the Id of the row created. 466 | /// 467 | /// The connection to use. 468 | /// The transaction to use. 469 | /// The command timeout to use. 470 | /// The table to insert into. 471 | /// The columns to set with this insert. 472 | /// The parameters to set for this insert. 473 | /// The key columns in this table. 474 | /// The entity to insert. 475 | /// The Id of the row created. 476 | public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 477 | { 478 | var sb = new StringBuilder(); 479 | sb.AppendFormat("INSERT INTO {0} ({1}) VALUES ({2})", tableName, columnList, parameterList); 480 | 481 | // If no primary key then safe to assume a join table with not too much data to return 482 | var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 483 | if (propertyInfos.Length == 0) 484 | { 485 | sb.Append(" RETURNING *"); 486 | } 487 | else 488 | { 489 | sb.Append(" RETURNING "); 490 | bool first = true; 491 | foreach (var property in propertyInfos) 492 | { 493 | if (!first) 494 | sb.Append(", "); 495 | first = false; 496 | sb.Append(property.Name); 497 | } 498 | } 499 | 500 | var results = await connection.QueryAsync(sb.ToString(), entityToInsert, transaction, commandTimeout).ConfigureAwait(false); 501 | 502 | // Return the key by assigning the corresponding property in the object - by product is that it supports compound primary keys 503 | var id = 0; 504 | foreach (var p in propertyInfos) 505 | { 506 | var value = ((IDictionary)results.First())[p.Name.ToLower()]; 507 | p.SetValue(entityToInsert, value, null); 508 | if (id == 0) 509 | id = Convert.ToInt32(value); 510 | } 511 | return id; 512 | } 513 | } 514 | 515 | public partial class SQLiteAdapter 516 | { 517 | /// 518 | /// Inserts into the database, returning the Id of the row created. 519 | /// 520 | /// The connection to use. 521 | /// The transaction to use. 522 | /// The command timeout to use. 523 | /// The table to insert into. 524 | /// The columns to set with this insert. 525 | /// The parameters to set for this insert. 526 | /// The key columns in this table. 527 | /// The entity to insert. 528 | /// The Id of the row created. 529 | public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 530 | { 531 | var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList}); SELECT last_insert_rowid() id"; 532 | var multi = await connection.QueryMultipleAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); 533 | 534 | var id = (int)(await multi.ReadFirstAsync().ConfigureAwait(false)).id; 535 | var pi = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 536 | if (pi.Length == 0) return id; 537 | 538 | var idp = pi[0]; 539 | idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); 540 | 541 | return id; 542 | } 543 | } 544 | 545 | public partial class FbAdapter 546 | { 547 | /// 548 | /// Inserts into the database, returning the Id of the row created. 549 | /// 550 | /// The connection to use. 551 | /// The transaction to use. 552 | /// The command timeout to use. 553 | /// The table to insert into. 554 | /// The columns to set with this insert. 555 | /// The parameters to set for this insert. 556 | /// The key columns in this table. 557 | /// The entity to insert. 558 | /// The Id of the row created. 559 | public async Task InsertAsync(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 560 | { 561 | var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; 562 | await connection.ExecuteAsync(cmd, entityToInsert, transaction, commandTimeout).ConfigureAwait(false); 563 | 564 | var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 565 | var keyName = propertyInfos[0].Name; 566 | var r = await connection.QueryAsync($"SELECT FIRST 1 {keyName} ID FROM {tableName} ORDER BY {keyName} DESC", transaction: transaction, commandTimeout: commandTimeout).ConfigureAwait(false); 567 | 568 | var id = r.First().ID; 569 | if (id == null) return 0; 570 | if (propertyInfos.Length == 0) return Convert.ToInt32(id); 571 | 572 | var idp = propertyInfos[0]; 573 | idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); 574 | 575 | return Convert.ToInt32(id); 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /src/Dapper.Contrib/SqlMapperExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Collections.Concurrent; 8 | using System.Reflection.Emit; 9 | using System.Threading; 10 | 11 | using Dapper; 12 | 13 | namespace Dapper.Contrib.Extensions 14 | { 15 | /// 16 | /// The Dapper.Contrib extensions for Dapper 17 | /// 18 | public static partial class SqlMapperExtensions 19 | { 20 | /// 21 | /// Defined a proxy object with a possibly dirty state. 22 | /// 23 | public interface IProxy //must be kept public 24 | { 25 | /// 26 | /// Whether the object has been changed. 27 | /// 28 | bool IsDirty { get; set; } 29 | } 30 | 31 | /// 32 | /// Defines a table name mapper for getting table names from types. 33 | /// 34 | public interface ITableNameMapper 35 | { 36 | /// 37 | /// Gets a table name from a given . 38 | /// 39 | /// The to get a name from. 40 | /// The table name for the given . 41 | string GetTableName(Type type); 42 | } 43 | 44 | /// 45 | /// The function to get a database type from the given . 46 | /// 47 | /// The connection to get a database type name from. 48 | public delegate string GetDatabaseTypeDelegate(IDbConnection connection); 49 | /// 50 | /// The function to get a table name from a given 51 | /// 52 | /// The to get a table name for. 53 | public delegate string TableNameMapperDelegate(Type type); 54 | 55 | private static readonly ConcurrentDictionary> KeyProperties = new ConcurrentDictionary>(); 56 | private static readonly ConcurrentDictionary> ExplicitKeyProperties = new ConcurrentDictionary>(); 57 | private static readonly ConcurrentDictionary> TypeProperties = new ConcurrentDictionary>(); 58 | private static readonly ConcurrentDictionary> ComputedProperties = new ConcurrentDictionary>(); 59 | private static readonly ConcurrentDictionary GetQueries = new ConcurrentDictionary(); 60 | private static readonly ConcurrentDictionary TypeTableName = new ConcurrentDictionary(); 61 | 62 | private static readonly ISqlAdapter DefaultAdapter = new SqlServerAdapter(); 63 | private static readonly Dictionary AdapterDictionary 64 | = new Dictionary(6) 65 | { 66 | ["sqlconnection"] = new SqlServerAdapter(), 67 | ["sqlceconnection"] = new SqlCeServerAdapter(), 68 | ["npgsqlconnection"] = new PostgresAdapter(), 69 | ["sqliteconnection"] = new SQLiteAdapter(), 70 | ["mysqlconnection"] = new MySqlAdapter(), 71 | ["fbconnection"] = new FbAdapter() 72 | }; 73 | 74 | private static List ComputedPropertiesCache(Type type) 75 | { 76 | if (ComputedProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) 77 | { 78 | return pi.ToList(); 79 | } 80 | 81 | var computedProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ComputedAttribute)).ToList(); 82 | 83 | ComputedProperties[type.TypeHandle] = computedProperties; 84 | return computedProperties; 85 | } 86 | 87 | private static List ExplicitKeyPropertiesCache(Type type) 88 | { 89 | if (ExplicitKeyProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) 90 | { 91 | return pi.ToList(); 92 | } 93 | 94 | var explicitKeyProperties = TypePropertiesCache(type).Where(p => p.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute)).ToList(); 95 | 96 | ExplicitKeyProperties[type.TypeHandle] = explicitKeyProperties; 97 | return explicitKeyProperties; 98 | } 99 | 100 | private static List KeyPropertiesCache(Type type) 101 | { 102 | if (KeyProperties.TryGetValue(type.TypeHandle, out IEnumerable pi)) 103 | { 104 | return pi.ToList(); 105 | } 106 | 107 | var allProperties = TypePropertiesCache(type); 108 | var keyProperties = allProperties.Where(p => p.GetCustomAttributes(true).Any(a => a is KeyAttribute)).ToList(); 109 | 110 | if (keyProperties.Count == 0) 111 | { 112 | var idProp = allProperties.Find(p => string.Equals(p.Name, "id", StringComparison.CurrentCultureIgnoreCase)); 113 | if (idProp != null && !idProp.GetCustomAttributes(true).Any(a => a is ExplicitKeyAttribute)) 114 | { 115 | keyProperties.Add(idProp); 116 | } 117 | } 118 | 119 | KeyProperties[type.TypeHandle] = keyProperties; 120 | return keyProperties; 121 | } 122 | 123 | private static List TypePropertiesCache(Type type) 124 | { 125 | if (TypeProperties.TryGetValue(type.TypeHandle, out IEnumerable pis)) 126 | { 127 | return pis.ToList(); 128 | } 129 | 130 | var properties = type.GetProperties().Where(IsWriteable).ToArray(); 131 | TypeProperties[type.TypeHandle] = properties; 132 | return properties.ToList(); 133 | } 134 | 135 | private static bool IsWriteable(PropertyInfo pi) 136 | { 137 | var attributes = pi.GetCustomAttributes(typeof(WriteAttribute), false).AsList(); 138 | if (attributes.Count != 1) return true; 139 | 140 | var writeAttribute = (WriteAttribute)attributes[0]; 141 | return writeAttribute.Write; 142 | } 143 | 144 | private static PropertyInfo GetSingleKey(string method) 145 | { 146 | var type = typeof(T); 147 | var keys = KeyPropertiesCache(type); 148 | var explicitKeys = ExplicitKeyPropertiesCache(type); 149 | var keyCount = keys.Count + explicitKeys.Count; 150 | if (keyCount > 1) 151 | throw new DataException($"{method} only supports an entity with a single [Key] or [ExplicitKey] property. [Key] Count: {keys.Count}, [ExplicitKey] Count: {explicitKeys.Count}"); 152 | if (keyCount == 0) 153 | throw new DataException($"{method} only supports an entity with a [Key] or an [ExplicitKey] property"); 154 | 155 | return keys.Count > 0 ? keys[0] : explicitKeys[0]; 156 | } 157 | 158 | /// 159 | /// Returns a single entity by a single id from table "Ts". 160 | /// Id must be marked with [Key] attribute. 161 | /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension 162 | /// for optimal performance. 163 | /// 164 | /// Interface or type to create and populate 165 | /// Open SqlConnection 166 | /// Id of the entity to get, must be marked with [Key] attribute 167 | /// The transaction to run under, null (the default) if none 168 | /// Number of seconds before command execution timeout 169 | /// Entity of T 170 | public static T Get(this IDbConnection connection, dynamic id, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 171 | { 172 | var type = typeof(T); 173 | 174 | if (!GetQueries.TryGetValue(type.TypeHandle, out string sql)) 175 | { 176 | var key = GetSingleKey(nameof(Get)); 177 | var name = GetTableName(type); 178 | 179 | sql = $"select * from {name} where {key.Name} = @id"; 180 | GetQueries[type.TypeHandle] = sql; 181 | } 182 | 183 | var dynParams = new DynamicParameters(); 184 | dynParams.Add("@id", id); 185 | 186 | T obj; 187 | 188 | if (type.IsInterface) 189 | { 190 | if (!(connection.Query(sql, dynParams).FirstOrDefault() is IDictionary res)) 191 | { 192 | return null; 193 | } 194 | 195 | obj = ProxyGenerator.GetInterfaceProxy(); 196 | 197 | foreach (var property in TypePropertiesCache(type)) 198 | { 199 | var val = res[property.Name]; 200 | if (val == null) continue; 201 | if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) 202 | { 203 | var genericType = Nullable.GetUnderlyingType(property.PropertyType); 204 | if (genericType != null) property.SetValue(obj, Convert.ChangeType(val, genericType), null); 205 | } 206 | else 207 | { 208 | property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); 209 | } 210 | } 211 | 212 | ((IProxy)obj).IsDirty = false; //reset change tracking and return 213 | } 214 | else 215 | { 216 | obj = connection.Query(sql, dynParams, transaction, commandTimeout: commandTimeout).FirstOrDefault(); 217 | } 218 | return obj; 219 | } 220 | 221 | /// 222 | /// Returns a list of entities from table "Ts". 223 | /// Id of T must be marked with [Key] attribute. 224 | /// Entities created from interfaces are tracked/intercepted for changes and used by the Update() extension 225 | /// for optimal performance. 226 | /// 227 | /// Interface or type to create and populate 228 | /// Open SqlConnection 229 | /// The transaction to run under, null (the default) if none 230 | /// Number of seconds before command execution timeout 231 | /// Entity of T 232 | public static IEnumerable GetAll(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 233 | { 234 | var type = typeof(T); 235 | var cacheType = typeof(List); 236 | 237 | if (!GetQueries.TryGetValue(cacheType.TypeHandle, out string sql)) 238 | { 239 | GetSingleKey(nameof(GetAll)); 240 | var name = GetTableName(type); 241 | 242 | sql = "select * from " + name; 243 | GetQueries[cacheType.TypeHandle] = sql; 244 | } 245 | 246 | if (!type.IsInterface) return connection.Query(sql, null, transaction, commandTimeout: commandTimeout); 247 | 248 | var result = connection.Query(sql); 249 | var list = new List(); 250 | foreach (IDictionary res in result) 251 | { 252 | var obj = ProxyGenerator.GetInterfaceProxy(); 253 | foreach (var property in TypePropertiesCache(type)) 254 | { 255 | var val = res[property.Name]; 256 | if (val == null) continue; 257 | if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) 258 | { 259 | var genericType = Nullable.GetUnderlyingType(property.PropertyType); 260 | if (genericType != null) property.SetValue(obj, Convert.ChangeType(val, genericType), null); 261 | } 262 | else 263 | { 264 | property.SetValue(obj, Convert.ChangeType(val, property.PropertyType), null); 265 | } 266 | } 267 | ((IProxy)obj).IsDirty = false; //reset change tracking and return 268 | list.Add(obj); 269 | } 270 | return list; 271 | } 272 | 273 | /// 274 | /// Specify a custom table name mapper based on the POCO type name 275 | /// 276 | #pragma warning disable CA2211 // Non-constant fields should not be visible - I agree with you, but we can't do that until we break the API 277 | public static TableNameMapperDelegate TableNameMapper; 278 | #pragma warning restore CA2211 // Non-constant fields should not be visible 279 | 280 | private static string GetTableName(Type type) 281 | { 282 | if (TypeTableName.TryGetValue(type.TypeHandle, out string name)) return name; 283 | 284 | if (TableNameMapper != null) 285 | { 286 | name = TableNameMapper(type); 287 | } 288 | else 289 | { 290 | //NOTE: This as dynamic trick falls back to handle both our own Table-attribute as well as the one in EntityFramework 291 | var tableAttrName = 292 | type.GetCustomAttribute(false)?.Name 293 | ?? (type.GetCustomAttributes(false).FirstOrDefault(attr => attr.GetType().Name == "TableAttribute") as dynamic)?.Name; 294 | 295 | if (tableAttrName != null) 296 | { 297 | name = tableAttrName; 298 | } 299 | else 300 | { 301 | name = type.Name + "s"; 302 | if (type.IsInterface && name.StartsWith("I")) 303 | name = name.Substring(1); 304 | } 305 | } 306 | 307 | TypeTableName[type.TypeHandle] = name; 308 | return name; 309 | } 310 | 311 | /// 312 | /// Inserts an entity into table "Ts" and returns identity id or number of inserted rows if inserting a list. 313 | /// 314 | /// The type to insert. 315 | /// Open SqlConnection 316 | /// Entity to insert, can be list of entities 317 | /// The transaction to run under, null (the default) if none 318 | /// Number of seconds before command execution timeout 319 | /// Identity of inserted entity, or number of inserted rows if inserting a list 320 | public static long Insert(this IDbConnection connection, T entityToInsert, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 321 | { 322 | var isList = false; 323 | 324 | var type = typeof(T); 325 | 326 | if (type.IsArray) 327 | { 328 | isList = true; 329 | type = type.GetElementType(); 330 | } 331 | else if (type.IsGenericType) 332 | { 333 | var typeInfo = type.GetTypeInfo(); 334 | bool implementsGenericIEnumerableOrIsGenericIEnumerable = 335 | typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || 336 | typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); 337 | 338 | if (implementsGenericIEnumerableOrIsGenericIEnumerable) 339 | { 340 | isList = true; 341 | type = type.GetGenericArguments()[0]; 342 | } 343 | } 344 | 345 | var name = GetTableName(type); 346 | var sbColumnList = new StringBuilder(null); 347 | var allProperties = TypePropertiesCache(type); 348 | var keyProperties = KeyPropertiesCache(type); 349 | var computedProperties = ComputedPropertiesCache(type); 350 | var allPropertiesExceptKeyAndComputed = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); 351 | 352 | var adapter = GetFormatter(connection); 353 | 354 | for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) 355 | { 356 | var property = allPropertiesExceptKeyAndComputed[i]; 357 | adapter.AppendColumnName(sbColumnList, property.Name); //fix for issue #336 358 | if (i < allPropertiesExceptKeyAndComputed.Count - 1) 359 | sbColumnList.Append(", "); 360 | } 361 | 362 | var sbParameterList = new StringBuilder(null); 363 | for (var i = 0; i < allPropertiesExceptKeyAndComputed.Count; i++) 364 | { 365 | var property = allPropertiesExceptKeyAndComputed[i]; 366 | sbParameterList.AppendFormat("@{0}", property.Name); 367 | if (i < allPropertiesExceptKeyAndComputed.Count - 1) 368 | sbParameterList.Append(", "); 369 | } 370 | 371 | int returnVal; 372 | var wasClosed = connection.State == ConnectionState.Closed; 373 | if (wasClosed) connection.Open(); 374 | 375 | if (!isList) //single entity 376 | { 377 | returnVal = adapter.Insert(connection, transaction, commandTimeout, name, sbColumnList.ToString(), 378 | sbParameterList.ToString(), keyProperties, entityToInsert); 379 | } 380 | else 381 | { 382 | //insert list of entities 383 | var cmd = $"insert into {name} ({sbColumnList}) values ({sbParameterList})"; 384 | returnVal = connection.Execute(cmd, entityToInsert, transaction, commandTimeout); 385 | } 386 | if (wasClosed) connection.Close(); 387 | return returnVal; 388 | } 389 | 390 | /// 391 | /// Updates entity in table "Ts", checks if the entity is modified if the entity is tracked by the Get() extension. 392 | /// 393 | /// Type to be updated 394 | /// Open SqlConnection 395 | /// Entity to be updated 396 | /// The transaction to run under, null (the default) if none 397 | /// Number of seconds before command execution timeout 398 | /// true if updated, false if not found or not modified (tracked entities) 399 | public static bool Update(this IDbConnection connection, T entityToUpdate, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 400 | { 401 | if (entityToUpdate is IProxy proxy && !proxy.IsDirty) 402 | { 403 | return false; 404 | } 405 | 406 | var type = typeof(T); 407 | 408 | if (type.IsArray) 409 | { 410 | type = type.GetElementType(); 411 | } 412 | else if (type.IsGenericType) 413 | { 414 | var typeInfo = type.GetTypeInfo(); 415 | bool implementsGenericIEnumerableOrIsGenericIEnumerable = 416 | typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || 417 | typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); 418 | 419 | if (implementsGenericIEnumerableOrIsGenericIEnumerable) 420 | { 421 | type = type.GetGenericArguments()[0]; 422 | } 423 | } 424 | 425 | var keyProperties = KeyPropertiesCache(type).ToList(); //added ToList() due to issue #418, must work on a list copy 426 | var explicitKeyProperties = ExplicitKeyPropertiesCache(type); 427 | if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) 428 | throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); 429 | 430 | var name = GetTableName(type); 431 | 432 | var sb = new StringBuilder(); 433 | sb.AppendFormat("update {0} set ", name); 434 | 435 | var allProperties = TypePropertiesCache(type); 436 | keyProperties.AddRange(explicitKeyProperties); 437 | var computedProperties = ComputedPropertiesCache(type); 438 | var nonIdProps = allProperties.Except(keyProperties.Union(computedProperties)).ToList(); 439 | 440 | var adapter = GetFormatter(connection); 441 | 442 | for (var i = 0; i < nonIdProps.Count; i++) 443 | { 444 | var property = nonIdProps[i]; 445 | adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336 446 | if (i < nonIdProps.Count - 1) 447 | sb.Append(", "); 448 | } 449 | sb.Append(" where "); 450 | for (var i = 0; i < keyProperties.Count; i++) 451 | { 452 | var property = keyProperties[i]; 453 | adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336 454 | if (i < keyProperties.Count - 1) 455 | sb.Append(" and "); 456 | } 457 | var updated = connection.Execute(sb.ToString(), entityToUpdate, commandTimeout: commandTimeout, transaction: transaction); 458 | return updated > 0; 459 | } 460 | 461 | /// 462 | /// Delete entity in table "Ts". 463 | /// 464 | /// Type of entity 465 | /// Open SqlConnection 466 | /// Entity to delete 467 | /// The transaction to run under, null (the default) if none 468 | /// Number of seconds before command execution timeout 469 | /// true if deleted, false if not found 470 | public static bool Delete(this IDbConnection connection, T entityToDelete, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 471 | { 472 | if (entityToDelete == null) 473 | throw new ArgumentException("Cannot Delete null Object", nameof(entityToDelete)); 474 | 475 | var type = typeof(T); 476 | 477 | if (type.IsArray) 478 | { 479 | type = type.GetElementType(); 480 | } 481 | else if (type.IsGenericType) 482 | { 483 | var typeInfo = type.GetTypeInfo(); 484 | bool implementsGenericIEnumerableOrIsGenericIEnumerable = 485 | typeInfo.ImplementedInterfaces.Any(ti => ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>)) || 486 | typeInfo.GetGenericTypeDefinition() == typeof(IEnumerable<>); 487 | 488 | if (implementsGenericIEnumerableOrIsGenericIEnumerable) 489 | { 490 | type = type.GetGenericArguments()[0]; 491 | } 492 | } 493 | 494 | var keyProperties = KeyPropertiesCache(type).ToList(); //added ToList() due to issue #418, must work on a list copy 495 | var explicitKeyProperties = ExplicitKeyPropertiesCache(type); 496 | if (keyProperties.Count == 0 && explicitKeyProperties.Count == 0) 497 | throw new ArgumentException("Entity must have at least one [Key] or [ExplicitKey] property"); 498 | 499 | var name = GetTableName(type); 500 | keyProperties.AddRange(explicitKeyProperties); 501 | 502 | var sb = new StringBuilder(); 503 | sb.AppendFormat("delete from {0} where ", name); 504 | 505 | var adapter = GetFormatter(connection); 506 | 507 | for (var i = 0; i < keyProperties.Count; i++) 508 | { 509 | var property = keyProperties[i]; 510 | adapter.AppendColumnNameEqualsValue(sb, property.Name); //fix for issue #336 511 | if (i < keyProperties.Count - 1) 512 | sb.Append(" and "); 513 | } 514 | var deleted = connection.Execute(sb.ToString(), entityToDelete, transaction, commandTimeout); 515 | return deleted > 0; 516 | } 517 | 518 | /// 519 | /// Delete all entities in the table related to the type T. 520 | /// 521 | /// Type of entity 522 | /// Open SqlConnection 523 | /// The transaction to run under, null (the default) if none 524 | /// Number of seconds before command execution timeout 525 | /// true if deleted, false if none found 526 | public static bool DeleteAll(this IDbConnection connection, IDbTransaction transaction = null, int? commandTimeout = null) where T : class 527 | { 528 | var type = typeof(T); 529 | var name = GetTableName(type); 530 | var statement = $"delete from {name}"; 531 | var deleted = connection.Execute(statement, null, transaction, commandTimeout); 532 | return deleted > 0; 533 | } 534 | 535 | /// 536 | /// Specifies a custom callback that detects the database type instead of relying on the default strategy (the name of the connection type object). 537 | /// Please note that this callback is global and will be used by all the calls that require a database specific adapter. 538 | /// 539 | #pragma warning disable CA2211 // Non-constant fields should not be visible - I agree with you, but we can't do that until we break the API 540 | public static GetDatabaseTypeDelegate GetDatabaseType; 541 | #pragma warning restore CA2211 // Non-constant fields should not be visible 542 | 543 | private static ISqlAdapter GetFormatter(IDbConnection connection) 544 | { 545 | var name = GetDatabaseType?.Invoke(connection).ToLower() 546 | ?? connection.GetType().Name.ToLower(); 547 | 548 | return AdapterDictionary.TryGetValue(name, out var adapter) 549 | ? adapter 550 | : DefaultAdapter; 551 | } 552 | 553 | private static class ProxyGenerator 554 | { 555 | private static readonly Dictionary TypeCache = new Dictionary(); 556 | 557 | private static AssemblyBuilder GetAsmBuilder(string name) 558 | { 559 | #if !NET461 560 | return AssemblyBuilder.DefineDynamicAssembly(new AssemblyName { Name = name }, AssemblyBuilderAccess.Run); 561 | #else 562 | return Thread.GetDomain().DefineDynamicAssembly(new AssemblyName { Name = name }, AssemblyBuilderAccess.Run); 563 | #endif 564 | } 565 | 566 | public static T GetInterfaceProxy() 567 | { 568 | Type typeOfT = typeof(T); 569 | 570 | if (TypeCache.TryGetValue(typeOfT, out Type k)) 571 | { 572 | return (T)Activator.CreateInstance(k); 573 | } 574 | var assemblyBuilder = GetAsmBuilder(typeOfT.Name); 575 | 576 | var moduleBuilder = assemblyBuilder.DefineDynamicModule("SqlMapperExtensions." + typeOfT.Name); //NOTE: to save, add "asdasd.dll" parameter 577 | 578 | var interfaceType = typeof(IProxy); 579 | var typeBuilder = moduleBuilder.DefineType(typeOfT.Name + "_" + Guid.NewGuid(), 580 | TypeAttributes.Public | TypeAttributes.Class); 581 | typeBuilder.AddInterfaceImplementation(typeOfT); 582 | typeBuilder.AddInterfaceImplementation(interfaceType); 583 | 584 | //create our _isDirty field, which implements IProxy 585 | var setIsDirtyMethod = CreateIsDirtyProperty(typeBuilder); 586 | 587 | // Generate a field for each property, which implements the T 588 | foreach (var property in typeof(T).GetProperties()) 589 | { 590 | var isId = property.GetCustomAttributes(true).Any(a => a is KeyAttribute); 591 | CreateProperty(typeBuilder, property.Name, property.PropertyType, setIsDirtyMethod, isId); 592 | } 593 | 594 | #if NETSTANDARD2_0 595 | var generatedType = typeBuilder.CreateTypeInfo().AsType(); 596 | #else 597 | var generatedType = typeBuilder.CreateType(); 598 | #endif 599 | 600 | TypeCache.Add(typeOfT, generatedType); 601 | return (T)Activator.CreateInstance(generatedType); 602 | } 603 | 604 | private static MethodInfo CreateIsDirtyProperty(TypeBuilder typeBuilder) 605 | { 606 | var propType = typeof(bool); 607 | var field = typeBuilder.DefineField("_" + nameof(IProxy.IsDirty), propType, FieldAttributes.Private); 608 | var property = typeBuilder.DefineProperty(nameof(IProxy.IsDirty), 609 | System.Reflection.PropertyAttributes.None, 610 | propType, 611 | new[] { propType }); 612 | 613 | const MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.SpecialName 614 | | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig; 615 | 616 | // Define the "get" and "set" accessor methods 617 | var currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + nameof(IProxy.IsDirty), 618 | getSetAttr, 619 | propType, 620 | Type.EmptyTypes); 621 | var currGetIl = currGetPropMthdBldr.GetILGenerator(); 622 | currGetIl.Emit(OpCodes.Ldarg_0); 623 | currGetIl.Emit(OpCodes.Ldfld, field); 624 | currGetIl.Emit(OpCodes.Ret); 625 | var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + nameof(IProxy.IsDirty), 626 | getSetAttr, 627 | null, 628 | new[] { propType }); 629 | var currSetIl = currSetPropMthdBldr.GetILGenerator(); 630 | currSetIl.Emit(OpCodes.Ldarg_0); 631 | currSetIl.Emit(OpCodes.Ldarg_1); 632 | currSetIl.Emit(OpCodes.Stfld, field); 633 | currSetIl.Emit(OpCodes.Ret); 634 | 635 | property.SetGetMethod(currGetPropMthdBldr); 636 | property.SetSetMethod(currSetPropMthdBldr); 637 | var getMethod = typeof(IProxy).GetMethod("get_" + nameof(IProxy.IsDirty)); 638 | var setMethod = typeof(IProxy).GetMethod("set_" + nameof(IProxy.IsDirty)); 639 | typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod); 640 | typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod); 641 | 642 | return currSetPropMthdBldr; 643 | } 644 | 645 | private static void CreateProperty(TypeBuilder typeBuilder, string propertyName, Type propType, MethodInfo setIsDirtyMethod, bool isIdentity) 646 | { 647 | //Define the field and the property 648 | var field = typeBuilder.DefineField("_" + propertyName, propType, FieldAttributes.Private); 649 | var property = typeBuilder.DefineProperty(propertyName, 650 | System.Reflection.PropertyAttributes.None, 651 | propType, 652 | new[] { propType }); 653 | 654 | const MethodAttributes getSetAttr = MethodAttributes.Public 655 | | MethodAttributes.Virtual 656 | | MethodAttributes.HideBySig; 657 | 658 | // Define the "get" and "set" accessor methods 659 | var currGetPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName, 660 | getSetAttr, 661 | propType, 662 | Type.EmptyTypes); 663 | 664 | var currGetIl = currGetPropMthdBldr.GetILGenerator(); 665 | currGetIl.Emit(OpCodes.Ldarg_0); 666 | currGetIl.Emit(OpCodes.Ldfld, field); 667 | currGetIl.Emit(OpCodes.Ret); 668 | 669 | var currSetPropMthdBldr = typeBuilder.DefineMethod("set_" + propertyName, 670 | getSetAttr, 671 | null, 672 | new[] { propType }); 673 | 674 | //store value in private field and set the isdirty flag 675 | var currSetIl = currSetPropMthdBldr.GetILGenerator(); 676 | currSetIl.Emit(OpCodes.Ldarg_0); 677 | currSetIl.Emit(OpCodes.Ldarg_1); 678 | currSetIl.Emit(OpCodes.Stfld, field); 679 | currSetIl.Emit(OpCodes.Ldarg_0); 680 | currSetIl.Emit(OpCodes.Ldc_I4_1); 681 | currSetIl.Emit(OpCodes.Call, setIsDirtyMethod); 682 | currSetIl.Emit(OpCodes.Ret); 683 | 684 | //TODO: Should copy all attributes defined by the interface? 685 | if (isIdentity) 686 | { 687 | var keyAttribute = typeof(KeyAttribute); 688 | var myConstructorInfo = keyAttribute.GetConstructor(Type.EmptyTypes); 689 | var attributeBuilder = new CustomAttributeBuilder(myConstructorInfo, Array.Empty()); 690 | property.SetCustomAttribute(attributeBuilder); 691 | } 692 | 693 | property.SetGetMethod(currGetPropMthdBldr); 694 | property.SetSetMethod(currSetPropMthdBldr); 695 | var getMethod = typeof(T).GetMethod("get_" + propertyName); 696 | var setMethod = typeof(T).GetMethod("set_" + propertyName); 697 | typeBuilder.DefineMethodOverride(currGetPropMthdBldr, getMethod); 698 | typeBuilder.DefineMethodOverride(currSetPropMthdBldr, setMethod); 699 | } 700 | } 701 | } 702 | 703 | /// 704 | /// Defines the name of a table to use in Dapper.Contrib commands. 705 | /// 706 | [AttributeUsage(AttributeTargets.Class)] 707 | public class TableAttribute : Attribute 708 | { 709 | /// 710 | /// Creates a table mapping to a specific name for Dapper.Contrib commands 711 | /// 712 | /// The name of this table in the database. 713 | public TableAttribute(string tableName) 714 | { 715 | Name = tableName; 716 | } 717 | 718 | /// 719 | /// The name of the table in the database 720 | /// 721 | public string Name { get; set; } 722 | } 723 | 724 | /// 725 | /// Specifies that this field is a primary key in the database 726 | /// 727 | [AttributeUsage(AttributeTargets.Property)] 728 | public class KeyAttribute : Attribute 729 | { 730 | } 731 | 732 | /// 733 | /// Specifies that this field is an explicitly set primary key in the database 734 | /// 735 | [AttributeUsage(AttributeTargets.Property)] 736 | public class ExplicitKeyAttribute : Attribute 737 | { 738 | } 739 | 740 | /// 741 | /// Specifies whether a field is writable in the database. 742 | /// 743 | [AttributeUsage(AttributeTargets.Property)] 744 | public class WriteAttribute : Attribute 745 | { 746 | /// 747 | /// Specifies whether a field is writable in the database. 748 | /// 749 | /// Whether a field is writable in the database. 750 | public WriteAttribute(bool write) 751 | { 752 | Write = write; 753 | } 754 | 755 | /// 756 | /// Whether a field is writable in the database. 757 | /// 758 | public bool Write { get; } 759 | } 760 | 761 | /// 762 | /// Specifies that this is a computed column. 763 | /// 764 | [AttributeUsage(AttributeTargets.Property)] 765 | public class ComputedAttribute : Attribute 766 | { 767 | } 768 | } 769 | 770 | /// 771 | /// The interface for all Dapper.Contrib database operations 772 | /// Implementing this is each provider's model. 773 | /// 774 | public partial interface ISqlAdapter 775 | { 776 | /// 777 | /// Inserts into the database, returning the Id of the row created. 778 | /// 779 | /// The connection to use. 780 | /// The transaction to use. 781 | /// The command timeout to use. 782 | /// The table to insert into. 783 | /// The columns to set with this insert. 784 | /// The parameters to set for this insert. 785 | /// The key columns in this table. 786 | /// The entity to insert. 787 | /// The Id of the row created. 788 | int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert); 789 | 790 | /// 791 | /// Adds the name of a column. 792 | /// 793 | /// The string builder to append to. 794 | /// The column name. 795 | void AppendColumnName(StringBuilder sb, string columnName); 796 | /// 797 | /// Adds a column equality to a parameter. 798 | /// 799 | /// The string builder to append to. 800 | /// The column name. 801 | void AppendColumnNameEqualsValue(StringBuilder sb, string columnName); 802 | } 803 | 804 | /// 805 | /// The SQL Server database adapter. 806 | /// 807 | public partial class SqlServerAdapter : ISqlAdapter 808 | { 809 | /// 810 | /// Inserts into the database, returning the Id of the row created. 811 | /// 812 | /// The connection to use. 813 | /// The transaction to use. 814 | /// The command timeout to use. 815 | /// The table to insert into. 816 | /// The columns to set with this insert. 817 | /// The parameters to set for this insert. 818 | /// The key columns in this table. 819 | /// The entity to insert. 820 | /// The Id of the row created. 821 | public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 822 | { 823 | var cmd = $"insert into {tableName} ({columnList}) values ({parameterList});select SCOPE_IDENTITY() id"; 824 | var multi = connection.QueryMultiple(cmd, entityToInsert, transaction, commandTimeout); 825 | 826 | var first = multi.Read().FirstOrDefault(); 827 | if (first == null || first.id == null) return 0; 828 | 829 | var id = (int)first.id; 830 | var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 831 | if (propertyInfos.Length == 0) return id; 832 | 833 | var idProperty = propertyInfos[0]; 834 | idProperty.SetValue(entityToInsert, Convert.ChangeType(id, idProperty.PropertyType), null); 835 | 836 | return id; 837 | } 838 | 839 | /// 840 | /// Adds the name of a column. 841 | /// 842 | /// The string builder to append to. 843 | /// The column name. 844 | public void AppendColumnName(StringBuilder sb, string columnName) 845 | { 846 | sb.AppendFormat("[{0}]", columnName); 847 | } 848 | 849 | /// 850 | /// Adds a column equality to a parameter. 851 | /// 852 | /// The string builder to append to. 853 | /// The column name. 854 | public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) 855 | { 856 | sb.AppendFormat("[{0}] = @{1}", columnName, columnName); 857 | } 858 | } 859 | 860 | /// 861 | /// The SQL Server Compact Edition database adapter. 862 | /// 863 | public partial class SqlCeServerAdapter : ISqlAdapter 864 | { 865 | /// 866 | /// Inserts into the database, returning the Id of the row created. 867 | /// 868 | /// The connection to use. 869 | /// The transaction to use. 870 | /// The command timeout to use. 871 | /// The table to insert into. 872 | /// The columns to set with this insert. 873 | /// The parameters to set for this insert. 874 | /// The key columns in this table. 875 | /// The entity to insert. 876 | /// The Id of the row created. 877 | public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 878 | { 879 | var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; 880 | connection.Execute(cmd, entityToInsert, transaction, commandTimeout); 881 | var r = connection.Query("select @@IDENTITY id", transaction: transaction, commandTimeout: commandTimeout).ToList(); 882 | 883 | if (r[0].id == null) return 0; 884 | var id = (int)r[0].id; 885 | 886 | var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 887 | if (propertyInfos.Length == 0) return id; 888 | 889 | var idProperty = propertyInfos[0]; 890 | idProperty.SetValue(entityToInsert, Convert.ChangeType(id, idProperty.PropertyType), null); 891 | 892 | return id; 893 | } 894 | 895 | /// 896 | /// Adds the name of a column. 897 | /// 898 | /// The string builder to append to. 899 | /// The column name. 900 | public void AppendColumnName(StringBuilder sb, string columnName) 901 | { 902 | sb.AppendFormat("[{0}]", columnName); 903 | } 904 | 905 | /// 906 | /// Adds a column equality to a parameter. 907 | /// 908 | /// The string builder to append to. 909 | /// The column name. 910 | public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) 911 | { 912 | sb.AppendFormat("[{0}] = @{1}", columnName, columnName); 913 | } 914 | } 915 | 916 | /// 917 | /// The MySQL database adapter. 918 | /// 919 | public partial class MySqlAdapter : ISqlAdapter 920 | { 921 | /// 922 | /// Inserts into the database, returning the Id of the row created. 923 | /// 924 | /// The connection to use. 925 | /// The transaction to use. 926 | /// The command timeout to use. 927 | /// The table to insert into. 928 | /// The columns to set with this insert. 929 | /// The parameters to set for this insert. 930 | /// The key columns in this table. 931 | /// The entity to insert. 932 | /// The Id of the row created. 933 | public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 934 | { 935 | var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; 936 | connection.Execute(cmd, entityToInsert, transaction, commandTimeout); 937 | var r = connection.Query("Select LAST_INSERT_ID() id", transaction: transaction, commandTimeout: commandTimeout); 938 | 939 | var id = r.First().id; 940 | if (id == null) return 0; 941 | var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 942 | if (propertyInfos.Length == 0) return Convert.ToInt32(id); 943 | 944 | var idp = propertyInfos[0]; 945 | idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); 946 | 947 | return Convert.ToInt32(id); 948 | } 949 | 950 | /// 951 | /// Adds the name of a column. 952 | /// 953 | /// The string builder to append to. 954 | /// The column name. 955 | public void AppendColumnName(StringBuilder sb, string columnName) 956 | { 957 | sb.AppendFormat("`{0}`", columnName); 958 | } 959 | 960 | /// 961 | /// Adds a column equality to a parameter. 962 | /// 963 | /// The string builder to append to. 964 | /// The column name. 965 | public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) 966 | { 967 | sb.AppendFormat("`{0}` = @{1}", columnName, columnName); 968 | } 969 | } 970 | 971 | /// 972 | /// The Postgres database adapter. 973 | /// 974 | public partial class PostgresAdapter : ISqlAdapter 975 | { 976 | /// 977 | /// Inserts into the database, returning the Id of the row created. 978 | /// 979 | /// The connection to use. 980 | /// The transaction to use. 981 | /// The command timeout to use. 982 | /// The table to insert into. 983 | /// The columns to set with this insert. 984 | /// The parameters to set for this insert. 985 | /// The key columns in this table. 986 | /// The entity to insert. 987 | /// The Id of the row created. 988 | public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 989 | { 990 | var sb = new StringBuilder(); 991 | sb.AppendFormat("insert into {0} ({1}) values ({2})", tableName, columnList, parameterList); 992 | 993 | // If no primary key then safe to assume a join table with not too much data to return 994 | var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 995 | if (propertyInfos.Length == 0) 996 | { 997 | sb.Append(" RETURNING *"); 998 | } 999 | else 1000 | { 1001 | sb.Append(" RETURNING "); 1002 | var first = true; 1003 | foreach (var property in propertyInfos) 1004 | { 1005 | if (!first) 1006 | sb.Append(", "); 1007 | first = false; 1008 | sb.Append(property.Name); 1009 | } 1010 | } 1011 | 1012 | var results = connection.Query(sb.ToString(), entityToInsert, transaction, commandTimeout: commandTimeout).ToList(); 1013 | 1014 | // Return the key by assigning the corresponding property in the object - by product is that it supports compound primary keys 1015 | var id = 0; 1016 | foreach (var p in propertyInfos) 1017 | { 1018 | var value = ((IDictionary)results[0])[p.Name.ToLower()]; 1019 | p.SetValue(entityToInsert, value, null); 1020 | if (id == 0) 1021 | id = Convert.ToInt32(value); 1022 | } 1023 | return id; 1024 | } 1025 | 1026 | /// 1027 | /// Adds the name of a column. 1028 | /// 1029 | /// The string builder to append to. 1030 | /// The column name. 1031 | public void AppendColumnName(StringBuilder sb, string columnName) 1032 | { 1033 | sb.AppendFormat("\"{0}\"", columnName); 1034 | } 1035 | 1036 | /// 1037 | /// Adds a column equality to a parameter. 1038 | /// 1039 | /// The string builder to append to. 1040 | /// The column name. 1041 | public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) 1042 | { 1043 | sb.AppendFormat("\"{0}\" = @{1}", columnName, columnName); 1044 | } 1045 | } 1046 | 1047 | /// 1048 | /// The SQLite database adapter. 1049 | /// 1050 | public partial class SQLiteAdapter : ISqlAdapter 1051 | { 1052 | /// 1053 | /// Inserts into the database, returning the Id of the row created. 1054 | /// 1055 | /// The connection to use. 1056 | /// The transaction to use. 1057 | /// The command timeout to use. 1058 | /// The table to insert into. 1059 | /// The columns to set with this insert. 1060 | /// The parameters to set for this insert. 1061 | /// The key columns in this table. 1062 | /// The entity to insert. 1063 | /// The Id of the row created. 1064 | public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 1065 | { 1066 | var cmd = $"INSERT INTO {tableName} ({columnList}) VALUES ({parameterList}); SELECT last_insert_rowid() id"; 1067 | var multi = connection.QueryMultiple(cmd, entityToInsert, transaction, commandTimeout); 1068 | 1069 | var id = (int)multi.Read().First().id; 1070 | var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 1071 | if (propertyInfos.Length == 0) return id; 1072 | 1073 | var idProperty = propertyInfos[0]; 1074 | idProperty.SetValue(entityToInsert, Convert.ChangeType(id, idProperty.PropertyType), null); 1075 | 1076 | return id; 1077 | } 1078 | 1079 | /// 1080 | /// Adds the name of a column. 1081 | /// 1082 | /// The string builder to append to. 1083 | /// The column name. 1084 | public void AppendColumnName(StringBuilder sb, string columnName) 1085 | { 1086 | sb.AppendFormat("\"{0}\"", columnName); 1087 | } 1088 | 1089 | /// 1090 | /// Adds a column equality to a parameter. 1091 | /// 1092 | /// The string builder to append to. 1093 | /// The column name. 1094 | public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) 1095 | { 1096 | sb.AppendFormat("\"{0}\" = @{1}", columnName, columnName); 1097 | } 1098 | } 1099 | 1100 | /// 1101 | /// The Firebase SQL adapter. 1102 | /// 1103 | public partial class FbAdapter : ISqlAdapter 1104 | { 1105 | /// 1106 | /// Inserts into the database, returning the Id of the row created. 1107 | /// 1108 | /// The connection to use. 1109 | /// The transaction to use. 1110 | /// The command timeout to use. 1111 | /// The table to insert into. 1112 | /// The columns to set with this insert. 1113 | /// The parameters to set for this insert. 1114 | /// The key columns in this table. 1115 | /// The entity to insert. 1116 | /// The Id of the row created. 1117 | public int Insert(IDbConnection connection, IDbTransaction transaction, int? commandTimeout, string tableName, string columnList, string parameterList, IEnumerable keyProperties, object entityToInsert) 1118 | { 1119 | var cmd = $"insert into {tableName} ({columnList}) values ({parameterList})"; 1120 | connection.Execute(cmd, entityToInsert, transaction, commandTimeout); 1121 | 1122 | var propertyInfos = keyProperties as PropertyInfo[] ?? keyProperties.ToArray(); 1123 | var keyName = propertyInfos[0].Name; 1124 | var r = connection.Query($"SELECT FIRST 1 {keyName} ID FROM {tableName} ORDER BY {keyName} DESC", transaction: transaction, commandTimeout: commandTimeout); 1125 | 1126 | var id = r.First().ID; 1127 | if (id == null) return 0; 1128 | if (propertyInfos.Length == 0) return Convert.ToInt32(id); 1129 | 1130 | var idp = propertyInfos[0]; 1131 | idp.SetValue(entityToInsert, Convert.ChangeType(id, idp.PropertyType), null); 1132 | 1133 | return Convert.ToInt32(id); 1134 | } 1135 | 1136 | /// 1137 | /// Adds the name of a column. 1138 | /// 1139 | /// The string builder to append to. 1140 | /// The column name. 1141 | public void AppendColumnName(StringBuilder sb, string columnName) 1142 | { 1143 | sb.AppendFormat("{0}", columnName); 1144 | } 1145 | 1146 | /// 1147 | /// Adds a column equality to a parameter. 1148 | /// 1149 | /// The string builder to append to. 1150 | /// The column name. 1151 | public void AppendColumnNameEqualsValue(StringBuilder sb, string columnName) 1152 | { 1153 | sb.AppendFormat("{0} = @{1}", columnName, columnName); 1154 | } 1155 | } 1156 | -------------------------------------------------------------------------------- /tests/Dapper.Tests.Contrib/Dapper.Tests.Contrib.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Dapper.Tests.Contrib 4 | Dapper Contrib Test Suite 5 | netcoreapp3.1;net462;net5.0 6 | $(NoWarn);CA1816;IDE0063;xUnit1004 7 | 8 | 9 | 10 | 11 | PreserveNewest 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/Dapper.Tests.Contrib/Helpers/Attributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit.Sdk; 3 | 4 | namespace Dapper.Tests 5 | { 6 | /// 7 | /// Override for that truncates our DisplayName down. 8 | /// 9 | /// Attribute that is applied to a method to indicate that it is a fact that should 10 | /// be run by the test runner. It can also be extended to support a customized definition 11 | /// of a test method. 12 | /// 13 | /// 14 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 15 | [XunitTestCaseDiscoverer("Dapper.Tests.FactDiscoverer", "Dapper.Tests.Contrib")] 16 | public class FactAttribute : Xunit.FactAttribute 17 | { 18 | } 19 | 20 | /// 21 | /// Override for that truncates our DisplayName down. 22 | /// 23 | /// Marks a test method as being a data theory. Data theories are tests which are 24 | /// fed various bits of data from a data source, mapping to parameters on the test 25 | /// method. If the data source contains multiple rows, then the test method is executed 26 | /// multiple times (once with each data row). Data is provided by attributes which 27 | /// derive from Xunit.Sdk.DataAttribute (notably, Xunit.InlineDataAttribute and Xunit.MemberDataAttribute). 28 | /// 29 | /// 30 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 31 | [XunitTestCaseDiscoverer("Dapper.Tests.TheoryDiscoverer", "Dapper.Tests.Contrib")] 32 | public class TheoryAttribute : Xunit.TheoryAttribute { } 33 | 34 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 35 | public sealed class FactLongRunningAttribute : FactAttribute 36 | { 37 | public FactLongRunningAttribute() 38 | { 39 | #if !LONG_RUNNING 40 | Skip = "Long running"; 41 | #endif 42 | } 43 | 44 | public string Url { get; private set; } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Dapper.Tests.Contrib/Helpers/XunitSkippable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Xunit.Abstractions; 8 | using Xunit.Sdk; 9 | 10 | namespace Dapper.Tests 11 | { 12 | public static class Skip 13 | { 14 | public static void Inconclusive(string reason = "inconclusive") 15 | => throw new SkipTestException(reason); 16 | 17 | public static void If(object obj, string reason = null) 18 | where T : class 19 | { 20 | if (obj is T) Skip.Inconclusive(reason ?? $"not valid for {typeof(T).FullName}"); 21 | } 22 | } 23 | 24 | #pragma warning disable RCS1194 // Implement exception constructors. 25 | public class SkipTestException : Exception 26 | { 27 | public SkipTestException(string reason) : base(reason) 28 | { 29 | } 30 | } 31 | #pragma warning restore RCS1194 // Implement exception constructors. 32 | 33 | public class FactDiscoverer : Xunit.Sdk.FactDiscoverer 34 | { 35 | public FactDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } 36 | 37 | protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute) 38 | => new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod); 39 | } 40 | 41 | public class TheoryDiscoverer : Xunit.Sdk.TheoryDiscoverer 42 | { 43 | public TheoryDiscoverer(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) { } 44 | 45 | protected override IEnumerable CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow) 46 | => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, dataRow) }; 47 | 48 | protected override IEnumerable CreateTestCasesForSkip(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, string skipReason) 49 | => new[] { new SkippableTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; 50 | 51 | protected override IEnumerable CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) 52 | => new[] { new SkippableTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod) }; 53 | 54 | protected override IEnumerable CreateTestCasesForSkippedDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow, string skipReason) 55 | => new[] { new NamedSkippedDataRowTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, skipReason, dataRow) }; 56 | } 57 | 58 | public class SkippableTestCase : XunitTestCase 59 | { 60 | protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => 61 | base.GetDisplayName(factAttribute, displayName).StripName(); 62 | 63 | [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] 64 | public SkippableTestCase() { } 65 | 66 | public SkippableTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, object[] testMethodArguments = null) 67 | : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, testMethodArguments) 68 | { 69 | } 70 | 71 | public override async Task RunAsync( 72 | IMessageSink diagnosticMessageSink, 73 | IMessageBus messageBus, 74 | object[] constructorArguments, 75 | ExceptionAggregator aggregator, 76 | CancellationTokenSource cancellationTokenSource) 77 | { 78 | var skipMessageBus = new SkippableMessageBus(messageBus); 79 | var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ConfigureAwait(false); 80 | return result.Update(skipMessageBus); 81 | } 82 | } 83 | 84 | public class SkippableTheoryTestCase : XunitTheoryTestCase 85 | { 86 | protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => 87 | base.GetDisplayName(factAttribute, displayName).StripName(); 88 | 89 | [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] 90 | public SkippableTheoryTestCase() { } 91 | 92 | public SkippableTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod) 93 | : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) { } 94 | 95 | public override async Task RunAsync( 96 | IMessageSink diagnosticMessageSink, 97 | IMessageBus messageBus, 98 | object[] constructorArguments, 99 | ExceptionAggregator aggregator, 100 | CancellationTokenSource cancellationTokenSource) 101 | { 102 | var skipMessageBus = new SkippableMessageBus(messageBus); 103 | var result = await base.RunAsync(diagnosticMessageSink, skipMessageBus, constructorArguments, aggregator, cancellationTokenSource).ConfigureAwait(false); 104 | return result.Update(skipMessageBus); 105 | } 106 | } 107 | 108 | public class NamedSkippedDataRowTestCase : XunitSkippedDataRowTestCase 109 | { 110 | protected override string GetDisplayName(IAttributeInfo factAttribute, string displayName) => 111 | base.GetDisplayName(factAttribute, displayName).StripName(); 112 | 113 | [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] 114 | public NamedSkippedDataRowTestCase() { } 115 | 116 | public NamedSkippedDataRowTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod, string skipReason, object[] testMethodArguments = null) 117 | : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, skipReason, testMethodArguments) { } 118 | } 119 | 120 | public class SkippableMessageBus : IMessageBus 121 | { 122 | private readonly IMessageBus InnerBus; 123 | public SkippableMessageBus(IMessageBus innerBus) => InnerBus = innerBus; 124 | 125 | public int DynamicallySkippedTestCount { get; private set; } 126 | 127 | public void Dispose() { } 128 | 129 | public bool QueueMessage(IMessageSinkMessage message) 130 | { 131 | if (message is ITestFailed testFailed) 132 | { 133 | var exceptionType = testFailed.ExceptionTypes.FirstOrDefault(); 134 | if (exceptionType == typeof(SkipTestException).FullName) 135 | { 136 | DynamicallySkippedTestCount++; 137 | return InnerBus.QueueMessage(new TestSkipped(testFailed.Test, testFailed.Messages.FirstOrDefault())); 138 | } 139 | } 140 | return InnerBus.QueueMessage(message); 141 | } 142 | } 143 | 144 | internal static class XUnitExtensions 145 | { 146 | internal static string StripName(this string name) => 147 | name.Replace("Dapper.Tests.", ""); 148 | 149 | public static RunSummary Update(this RunSummary summary, SkippableMessageBus bus) 150 | { 151 | if (bus.DynamicallySkippedTestCount > 0) 152 | { 153 | summary.Failed -= bus.DynamicallySkippedTestCount; 154 | summary.Skipped += bus.DynamicallySkippedTestCount; 155 | } 156 | return summary; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/Dapper.Tests.Contrib/TestSuite.Async.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using Dapper.Contrib.Extensions; 7 | using FactAttribute = Dapper.Tests.Contrib.SkippableFactAttribute; 8 | using Xunit; 9 | 10 | namespace Dapper.Tests.Contrib 11 | { 12 | public abstract partial class TestSuite 13 | { 14 | [Fact] 15 | public async Task TypeWithGenericParameterCanBeInsertedAsync() 16 | { 17 | using (var connection = GetOpenConnection()) 18 | { 19 | await connection.DeleteAllAsync>(); 20 | var objectToInsert = new GenericType 21 | { 22 | Id = Guid.NewGuid().ToString(), 23 | Name = "something" 24 | }; 25 | await connection.InsertAsync(objectToInsert); 26 | 27 | Assert.Single(connection.GetAll>()); 28 | 29 | var objectsToInsert = new List>(2) 30 | { 31 | new GenericType 32 | { 33 | Id = Guid.NewGuid().ToString(), 34 | Name = "1", 35 | }, 36 | new GenericType 37 | { 38 | Id = Guid.NewGuid().ToString(), 39 | Name = "2", 40 | } 41 | }; 42 | 43 | await connection.InsertAsync(objectsToInsert); 44 | var list = connection.GetAll>(); 45 | Assert.Equal(3, list.Count()); 46 | } 47 | } 48 | 49 | [Fact] 50 | public async Task TypeWithGenericParameterCanBeUpdatedAsync() 51 | { 52 | using (var connection = GetOpenConnection()) 53 | { 54 | var objectToInsert = new GenericType 55 | { 56 | Id = Guid.NewGuid().ToString(), 57 | Name = "something" 58 | }; 59 | await connection.InsertAsync(objectToInsert); 60 | 61 | objectToInsert.Name = "somethingelse"; 62 | await connection.UpdateAsync(objectToInsert); 63 | 64 | var updatedObject = connection.Get>(objectToInsert.Id); 65 | Assert.Equal(objectToInsert.Name, updatedObject.Name); 66 | } 67 | } 68 | 69 | [Fact] 70 | public async Task TypeWithGenericParameterCanBeDeletedAsync() 71 | { 72 | using (var connection = GetOpenConnection()) 73 | { 74 | var objectToInsert = new GenericType 75 | { 76 | Id = Guid.NewGuid().ToString(), 77 | Name = "something" 78 | }; 79 | await connection.InsertAsync(objectToInsert); 80 | 81 | bool deleted = await connection.DeleteAsync(objectToInsert); 82 | Assert.True(deleted); 83 | } 84 | } 85 | 86 | [Fact] 87 | public async Task GetAsyncSucceedsAfterDeleteAsyncWhenExplicitKeyPresent() 88 | { 89 | using (var connection = GetOpenConnection()) 90 | { 91 | await connection.DeleteAsync(new ObjectX { ObjectXId = Guid.NewGuid().ToString() }).ConfigureAwait(false); 92 | var retrieved = await connection.GetAsync(Guid.NewGuid().ToString()).ConfigureAwait(false); 93 | Assert.Null(retrieved); 94 | } 95 | } 96 | 97 | /// 98 | /// Tests for issue #351 99 | /// 100 | [Fact] 101 | public async Task InsertGetUpdateDeleteWithExplicitKeyAsync() 102 | { 103 | using (var connection = GetOpenConnection()) 104 | { 105 | var guid = Guid.NewGuid().ToString(); 106 | var o1 = new ObjectX { ObjectXId = guid, Name = "Foo" }; 107 | var originalxCount = (await connection.QueryAsync("Select Count(*) From ObjectX").ConfigureAwait(false)).First(); 108 | await connection.InsertAsync(o1).ConfigureAwait(false); 109 | var list1 = (await connection.QueryAsync("select * from ObjectX").ConfigureAwait(false)).ToList(); 110 | Assert.Equal(list1.Count, originalxCount + 1); 111 | o1 = await connection.GetAsync(guid).ConfigureAwait(false); 112 | Assert.Equal(o1.ObjectXId, guid); 113 | o1.Name = "Bar"; 114 | await connection.UpdateAsync(o1).ConfigureAwait(false); 115 | o1 = await connection.GetAsync(guid).ConfigureAwait(false); 116 | Assert.Equal("Bar", o1.Name); 117 | await connection.DeleteAsync(o1).ConfigureAwait(false); 118 | o1 = await connection.GetAsync(guid).ConfigureAwait(false); 119 | Assert.Null(o1); 120 | 121 | const int id = 42; 122 | var o2 = new ObjectY { ObjectYId = id, Name = "Foo" }; 123 | var originalyCount = connection.Query("Select Count(*) From ObjectY").First(); 124 | await connection.InsertAsync(o2).ConfigureAwait(false); 125 | var list2 = (await connection.QueryAsync("select * from ObjectY").ConfigureAwait(false)).ToList(); 126 | Assert.Equal(list2.Count, originalyCount + 1); 127 | o2 = await connection.GetAsync(id).ConfigureAwait(false); 128 | Assert.Equal(o2.ObjectYId, id); 129 | o2.Name = "Bar"; 130 | await connection.UpdateAsync(o2).ConfigureAwait(false); 131 | o2 = await connection.GetAsync(id).ConfigureAwait(false); 132 | Assert.Equal("Bar", o2.Name); 133 | await connection.DeleteAsync(o2).ConfigureAwait(false); 134 | o2 = await connection.GetAsync(id).ConfigureAwait(false); 135 | Assert.Null(o2); 136 | } 137 | } 138 | 139 | [Fact] 140 | public async Task TableNameAsync() 141 | { 142 | using (var connection = GetOpenConnection()) 143 | { 144 | // tests against "Automobiles" table (Table attribute) 145 | var id = await connection.InsertAsync(new Car { Name = "VolvoAsync" }).ConfigureAwait(false); 146 | var car = await connection.GetAsync(id).ConfigureAwait(false); 147 | Assert.NotNull(car); 148 | Assert.Equal("VolvoAsync", car.Name); 149 | Assert.True(await connection.UpdateAsync(new Car { Id = id, Name = "SaabAsync" }).ConfigureAwait(false)); 150 | Assert.Equal("SaabAsync", (await connection.GetAsync(id).ConfigureAwait(false)).Name); 151 | Assert.True(await connection.DeleteAsync(new Car { Id = id }).ConfigureAwait(false)); 152 | Assert.Null(await connection.GetAsync(id).ConfigureAwait(false)); 153 | } 154 | } 155 | 156 | [Fact] 157 | public async Task TestSimpleGetAsync() 158 | { 159 | using (var connection = GetOpenConnection()) 160 | { 161 | var id = await connection.InsertAsync(new User { Name = "Adama", Age = 10 }).ConfigureAwait(false); 162 | var user = await connection.GetAsync(id).ConfigureAwait(false); 163 | Assert.Equal(id, user.Id); 164 | Assert.Equal("Adama", user.Name); 165 | await connection.DeleteAsync(user).ConfigureAwait(false); 166 | } 167 | } 168 | 169 | [Fact] 170 | public async Task InsertGetUpdateAsync() 171 | { 172 | using (var connection = GetOpenConnection()) 173 | { 174 | Assert.Null(await connection.GetAsync(30).ConfigureAwait(false)); 175 | 176 | var originalCount = (await connection.QueryAsync("select Count(*) from Users").ConfigureAwait(false)).First(); 177 | 178 | var id = await connection.InsertAsync(new User { Name = "Adam", Age = 10 }).ConfigureAwait(false); 179 | 180 | //get a user with "isdirty" tracking 181 | var user = await connection.GetAsync(id).ConfigureAwait(false); 182 | Assert.Equal("Adam", user.Name); 183 | Assert.False(await connection.UpdateAsync(user).ConfigureAwait(false)); //returns false if not updated, based on tracking 184 | user.Name = "Bob"; 185 | Assert.True(await connection.UpdateAsync(user).ConfigureAwait(false)); //returns true if updated, based on tracking 186 | user = await connection.GetAsync(id).ConfigureAwait(false); 187 | Assert.Equal("Bob", user.Name); 188 | 189 | //get a user with no tracking 190 | var notrackedUser = await connection.GetAsync(id).ConfigureAwait(false); 191 | Assert.Equal("Bob", notrackedUser.Name); 192 | Assert.True(await connection.UpdateAsync(notrackedUser).ConfigureAwait(false)); 193 | //returns true, even though user was not changed 194 | notrackedUser.Name = "Cecil"; 195 | Assert.True(await connection.UpdateAsync(notrackedUser).ConfigureAwait(false)); 196 | Assert.Equal("Cecil", (await connection.GetAsync(id).ConfigureAwait(false)).Name); 197 | 198 | Assert.Equal((await connection.QueryAsync("select * from Users").ConfigureAwait(false)).Count(), originalCount + 1); 199 | Assert.True(await connection.DeleteAsync(user).ConfigureAwait(false)); 200 | Assert.Equal((await connection.QueryAsync("select * from Users").ConfigureAwait(false)).Count(), originalCount); 201 | 202 | Assert.False(await connection.UpdateAsync(notrackedUser).ConfigureAwait(false)); //returns false, user not found 203 | 204 | Assert.True(await connection.InsertAsync(new User { Name = "Adam", Age = 10 }).ConfigureAwait(false) > originalCount + 1); 205 | } 206 | } 207 | 208 | [Fact] 209 | public async Task InsertCheckKeyAsync() 210 | { 211 | using (var connection = GetOpenConnection()) 212 | { 213 | await connection.DeleteAllAsync().ConfigureAwait(false); 214 | 215 | Assert.Null(await connection.GetAsync(3).ConfigureAwait(false)); 216 | var user = new User { Name = "Adamb", Age = 10 }; 217 | var id = await connection.InsertAsync(user).ConfigureAwait(false); 218 | Assert.Equal(user.Id, id); 219 | } 220 | } 221 | 222 | [Fact] 223 | public async Task BuilderSelectClauseAsync() 224 | { 225 | using (var connection = GetOpenConnection()) 226 | { 227 | await connection.DeleteAllAsync().ConfigureAwait(false); 228 | 229 | var rand = new Random(8675309); 230 | var data = new List(100); 231 | for (var i = 0; i < 100; i++) 232 | { 233 | var nU = new User { Age = rand.Next(70), Id = i, Name = Guid.NewGuid().ToString() }; 234 | data.Add(nU); 235 | nU.Id = await connection.InsertAsync(nU).ConfigureAwait(false); 236 | } 237 | 238 | var builder = new SqlBuilder(); 239 | var justId = builder.AddTemplate("SELECT /**select**/ FROM Users"); 240 | var all = builder.AddTemplate("SELECT Name, /**select**/, Age FROM Users"); 241 | 242 | builder.Select("Id"); 243 | 244 | var ids = await connection.QueryAsync(justId.RawSql, justId.Parameters).ConfigureAwait(false); 245 | var users = await connection.QueryAsync(all.RawSql, all.Parameters).ConfigureAwait(false); 246 | 247 | foreach (var u in data) 248 | { 249 | if (!ids.Any(i => u.Id == i)) throw new Exception("Missing ids in select"); 250 | if (!users.Any(a => a.Id == u.Id && a.Name == u.Name && a.Age == u.Age)) 251 | throw new Exception("Missing users in select"); 252 | } 253 | } 254 | } 255 | 256 | [Fact] 257 | public async Task BuilderTemplateWithoutCompositionAsync() 258 | { 259 | var builder = new SqlBuilder(); 260 | var template = builder.AddTemplate("SELECT COUNT(*) FROM Users WHERE Age = @age", new { age = 5 }); 261 | 262 | if (template.RawSql == null) throw new Exception("RawSql null"); 263 | if (template.Parameters == null) throw new Exception("Parameters null"); 264 | 265 | using (var connection = GetOpenConnection()) 266 | { 267 | await connection.DeleteAllAsync().ConfigureAwait(false); 268 | 269 | await connection.InsertAsync(new User { Age = 5, Name = "Testy McTestington" }).ConfigureAwait(false); 270 | 271 | if ((await connection.QueryAsync(template.RawSql, template.Parameters).ConfigureAwait(false)).Single() != 1) 272 | throw new Exception("Query failed"); 273 | } 274 | } 275 | 276 | [Fact] 277 | public async Task InsertEnumerableAsync() 278 | { 279 | await InsertHelperAsync(src => src.AsEnumerable()).ConfigureAwait(false); 280 | } 281 | 282 | [Fact] 283 | public async Task InsertArrayAsync() 284 | { 285 | await InsertHelperAsync(src => src.ToArray()).ConfigureAwait(false); 286 | } 287 | 288 | [Fact] 289 | public async Task InsertListAsync() 290 | { 291 | await InsertHelperAsync(src => src.ToList()).ConfigureAwait(false); 292 | } 293 | 294 | private async Task InsertHelperAsync(Func, T> helper) 295 | where T : class 296 | { 297 | const int numberOfEntities = 10; 298 | 299 | var users = new List(numberOfEntities); 300 | for (var i = 0; i < numberOfEntities; i++) 301 | users.Add(new User { Name = "User " + i, Age = i }); 302 | 303 | using (var connection = GetOpenConnection()) 304 | { 305 | await connection.DeleteAllAsync().ConfigureAwait(false); 306 | 307 | var total = await connection.InsertAsync(helper(users)).ConfigureAwait(false); 308 | Assert.Equal(total, numberOfEntities); 309 | users = connection.Query("select * from Users").ToList(); 310 | Assert.Equal(users.Count, numberOfEntities); 311 | } 312 | } 313 | 314 | [Fact] 315 | public async Task UpdateEnumerableAsync() 316 | { 317 | await UpdateHelperAsync(src => src.AsEnumerable()).ConfigureAwait(false); 318 | } 319 | 320 | [Fact] 321 | public async Task UpdateArrayAsync() 322 | { 323 | await UpdateHelperAsync(src => src.ToArray()).ConfigureAwait(false); 324 | } 325 | 326 | [Fact] 327 | public async Task UpdateListAsync() 328 | { 329 | await UpdateHelperAsync(src => src.ToList()).ConfigureAwait(false); 330 | } 331 | 332 | private async Task UpdateHelperAsync(Func, T> helper) 333 | where T : class 334 | { 335 | const int numberOfEntities = 10; 336 | 337 | var users = new List(numberOfEntities); 338 | for (var i = 0; i < numberOfEntities; i++) 339 | users.Add(new User { Name = "User " + i, Age = i }); 340 | 341 | using (var connection = GetOpenConnection()) 342 | { 343 | await connection.DeleteAllAsync().ConfigureAwait(false); 344 | 345 | var total = await connection.InsertAsync(helper(users)).ConfigureAwait(false); 346 | Assert.Equal(total, numberOfEntities); 347 | users = connection.Query("select * from Users").ToList(); 348 | Assert.Equal(users.Count, numberOfEntities); 349 | foreach (var user in users) 350 | { 351 | user.Name += " updated"; 352 | } 353 | await connection.UpdateAsync(helper(users)).ConfigureAwait(false); 354 | var name = connection.Query("select * from Users").First().Name; 355 | Assert.Contains("updated", name); 356 | } 357 | } 358 | 359 | [Fact] 360 | public async Task DeleteEnumerableAsync() 361 | { 362 | await DeleteHelperAsync(src => src.AsEnumerable()).ConfigureAwait(false); 363 | } 364 | 365 | [Fact] 366 | public async Task DeleteArrayAsync() 367 | { 368 | await DeleteHelperAsync(src => src.ToArray()).ConfigureAwait(false); 369 | } 370 | 371 | [Fact] 372 | public async Task DeleteListAsync() 373 | { 374 | await DeleteHelperAsync(src => src.ToList()).ConfigureAwait(false); 375 | } 376 | 377 | private async Task DeleteHelperAsync(Func, T> helper) 378 | where T : class 379 | { 380 | const int numberOfEntities = 10; 381 | 382 | var users = new List(numberOfEntities); 383 | for (var i = 0; i < numberOfEntities; i++) 384 | users.Add(new User { Name = "User " + i, Age = i }); 385 | 386 | using (var connection = GetOpenConnection()) 387 | { 388 | await connection.DeleteAllAsync().ConfigureAwait(false); 389 | 390 | var total = await connection.InsertAsync(helper(users)).ConfigureAwait(false); 391 | Assert.Equal(total, numberOfEntities); 392 | users = connection.Query("select * from Users").ToList(); 393 | Assert.Equal(users.Count, numberOfEntities); 394 | 395 | var usersToDelete = users.Take(10).ToList(); 396 | await connection.DeleteAsync(helper(usersToDelete)).ConfigureAwait(false); 397 | users = connection.Query("select * from Users").ToList(); 398 | Assert.Equal(users.Count, numberOfEntities - 10); 399 | } 400 | } 401 | 402 | [Fact] 403 | public async Task GetAllAsync() 404 | { 405 | const int numberOfEntities = 10; 406 | 407 | var users = new List(numberOfEntities); 408 | for (var i = 0; i < numberOfEntities; i++) 409 | users.Add(new User { Name = "User " + i, Age = i }); 410 | 411 | using (var connection = GetOpenConnection()) 412 | { 413 | await connection.DeleteAllAsync().ConfigureAwait(false); 414 | 415 | var total = await connection.InsertAsync(users).ConfigureAwait(false); 416 | Assert.Equal(total, numberOfEntities); 417 | users = (List)await connection.GetAllAsync().ConfigureAwait(false); 418 | Assert.Equal(users.Count, numberOfEntities); 419 | var iusers = await connection.GetAllAsync().ConfigureAwait(false); 420 | Assert.Equal(iusers.ToList().Count, numberOfEntities); 421 | } 422 | } 423 | 424 | /// 425 | /// Test for issue #933 426 | /// 427 | [Fact] 428 | public async void GetAsyncAndGetAllAsyncWithNullableValues() 429 | { 430 | using (var connection = GetOpenConnection()) 431 | { 432 | var id1 = connection.Insert(new NullableDate { DateValue = new DateTime(2011, 07, 14) }); 433 | var id2 = connection.Insert(new NullableDate { DateValue = null }); 434 | 435 | var value1 = await connection.GetAsync(id1).ConfigureAwait(false); 436 | Assert.Equal(new DateTime(2011, 07, 14), value1.DateValue.Value); 437 | 438 | var value2 = await connection.GetAsync(id2).ConfigureAwait(false); 439 | Assert.True(value2.DateValue == null); 440 | 441 | var value3 = await connection.GetAllAsync().ConfigureAwait(false); 442 | var valuesList = value3.ToList(); 443 | Assert.Equal(new DateTime(2011, 07, 14), valuesList[0].DateValue.Value); 444 | Assert.True(valuesList[1].DateValue == null); 445 | } 446 | } 447 | 448 | [Fact] 449 | public async Task InsertFieldWithReservedNameAsync() 450 | { 451 | using (var connection = GetOpenConnection()) 452 | { 453 | await connection.DeleteAllAsync().ConfigureAwait(false); 454 | var id = await connection.InsertAsync(new Result { Name = "Adam", Order = 1 }).ConfigureAwait(false); 455 | 456 | var result = await connection.GetAsync(id).ConfigureAwait(false); 457 | Assert.Equal(1, result.Order); 458 | } 459 | } 460 | 461 | [Fact] 462 | public async Task DeleteAllAsync() 463 | { 464 | using (var connection = GetOpenConnection()) 465 | { 466 | await connection.DeleteAllAsync().ConfigureAwait(false); 467 | 468 | var id1 = await connection.InsertAsync(new User { Name = "Alice", Age = 32 }).ConfigureAwait(false); 469 | var id2 = await connection.InsertAsync(new User { Name = "Bob", Age = 33 }).ConfigureAwait(false); 470 | await connection.DeleteAllAsync().ConfigureAwait(false); 471 | Assert.Null(await connection.GetAsync(id1).ConfigureAwait(false)); 472 | Assert.Null(await connection.GetAsync(id2).ConfigureAwait(false)); 473 | } 474 | } 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /tests/Dapper.Tests.Contrib/TestSuite.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using Dapper.Contrib.Extensions; 6 | using Xunit; 7 | 8 | using FactAttribute = Dapper.Tests.Contrib.SkippableFactAttribute; 9 | 10 | namespace Dapper.Tests.Contrib 11 | { 12 | [Table("ObjectX")] 13 | public class ObjectX 14 | { 15 | [ExplicitKey] 16 | public string ObjectXId { get; set; } 17 | public string Name { get; set; } 18 | } 19 | 20 | [Table("ObjectY")] 21 | public class ObjectY 22 | { 23 | [ExplicitKey] 24 | public int ObjectYId { get; set; } 25 | public string Name { get; set; } 26 | } 27 | 28 | [Table("ObjectZ")] 29 | public class ObjectZ 30 | { 31 | [ExplicitKey] 32 | public int Id { get; set; } 33 | public string Name { get; set; } 34 | } 35 | 36 | public interface IUser 37 | { 38 | [Key] 39 | int Id { get; set; } 40 | string Name { get; set; } 41 | int Age { get; set; } 42 | } 43 | 44 | public class User : IUser 45 | { 46 | public int Id { get; set; } 47 | public string Name { get; set; } 48 | public int Age { get; set; } 49 | } 50 | 51 | public interface INullableDate 52 | { 53 | [Key] 54 | int Id { get; set; } 55 | DateTime? DateValue { get; set; } 56 | } 57 | 58 | public class NullableDate : INullableDate 59 | { 60 | public int Id { get; set; } 61 | public DateTime? DateValue { get; set; } 62 | } 63 | 64 | public class Person 65 | { 66 | public int Id { get; set; } 67 | public string Name { get; set; } 68 | } 69 | 70 | [Table("Stuff")] 71 | public class Stuff 72 | { 73 | [Key] 74 | public short TheId { get; set; } 75 | public string Name { get; set; } 76 | public DateTime? Created { get; set; } 77 | } 78 | 79 | [Table("Automobiles")] 80 | public class Car 81 | { 82 | public int Id { get; set; } 83 | public string Name { get; set; } 84 | [Computed] 85 | public string Computed { get; set; } 86 | } 87 | 88 | [Table("Results")] 89 | public class Result 90 | { 91 | public int Id { get; set; } 92 | public string Name { get; set; } 93 | public int Order { get; set; } 94 | } 95 | 96 | [Table("GenericType")] 97 | public class GenericType 98 | { 99 | [ExplicitKey] 100 | public string Id { get; set; } 101 | public string Name { get; set; } 102 | } 103 | 104 | public abstract partial class TestSuite 105 | { 106 | public abstract IDbConnection GetConnection(); 107 | 108 | protected static string GetConnectionString(string name, string defaultConnectionString) => 109 | Environment.GetEnvironmentVariable(name) ?? defaultConnectionString; 110 | 111 | private IDbConnection GetOpenConnection() 112 | { 113 | var connection = GetConnection(); 114 | connection.Open(); 115 | return connection; 116 | } 117 | 118 | [Fact] 119 | public void TypeWithGenericParameterCanBeInserted() 120 | { 121 | using (var connection = GetOpenConnection()) 122 | { 123 | connection.DeleteAll>(); 124 | var objectToInsert = new GenericType 125 | { 126 | Id = Guid.NewGuid().ToString(), 127 | Name = "something" 128 | }; 129 | connection.Insert(objectToInsert); 130 | 131 | Assert.Single(connection.GetAll>()); 132 | 133 | var objectsToInsert = new List>(2) 134 | { 135 | new GenericType 136 | { 137 | Id = Guid.NewGuid().ToString(), 138 | Name = "1", 139 | }, 140 | new GenericType 141 | { 142 | Id = Guid.NewGuid().ToString(), 143 | Name = "2", 144 | } 145 | }; 146 | 147 | connection.Insert(objectsToInsert); 148 | var list = connection.GetAll>(); 149 | Assert.Equal(3, list.Count()); 150 | } 151 | } 152 | 153 | [Fact] 154 | public void TypeWithGenericParameterCanBeUpdated() 155 | { 156 | using (var connection = GetOpenConnection()) 157 | { 158 | var objectToInsert = new GenericType 159 | { 160 | Id = Guid.NewGuid().ToString(), 161 | Name = "something" 162 | }; 163 | connection.Insert(objectToInsert); 164 | 165 | objectToInsert.Name = "somethingelse"; 166 | connection.Update(objectToInsert); 167 | 168 | var updatedObject = connection.Get>(objectToInsert.Id); 169 | Assert.Equal(objectToInsert.Name, updatedObject.Name); 170 | } 171 | } 172 | 173 | [Fact] 174 | public void TypeWithGenericParameterCanBeDeleted() 175 | { 176 | using (var connection = GetOpenConnection()) 177 | { 178 | var objectToInsert = new GenericType 179 | { 180 | Id = Guid.NewGuid().ToString(), 181 | Name = "something" 182 | }; 183 | connection.Insert(objectToInsert); 184 | 185 | bool deleted = connection.Delete(objectToInsert); 186 | Assert.True(deleted); 187 | } 188 | } 189 | 190 | [Fact] 191 | public void Issue418() 192 | { 193 | using (var connection = GetOpenConnection()) 194 | { 195 | //update first (will fail) then insert 196 | //added for bug #418 197 | var updateObject = new ObjectX 198 | { 199 | ObjectXId = Guid.NewGuid().ToString(), 200 | Name = "Someone" 201 | }; 202 | var updates = connection.Update(updateObject); 203 | Assert.False(updates); 204 | 205 | connection.DeleteAll(); 206 | 207 | var objectXId = Guid.NewGuid().ToString(); 208 | var insertObject = new ObjectX 209 | { 210 | ObjectXId = objectXId, 211 | Name = "Someone else" 212 | }; 213 | connection.Insert(insertObject); 214 | var list = connection.GetAll(); 215 | Assert.Single(list); 216 | } 217 | } 218 | 219 | /// 220 | /// Tests for issue #351 221 | /// 222 | [Fact] 223 | public void InsertGetUpdateDeleteWithExplicitKey() 224 | { 225 | using (var connection = GetOpenConnection()) 226 | { 227 | var guid = Guid.NewGuid().ToString(); 228 | var o1 = new ObjectX { ObjectXId = guid, Name = "Foo" }; 229 | var originalxCount = connection.Query("Select Count(*) From ObjectX").First(); 230 | connection.Insert(o1); 231 | var list1 = connection.Query("select * from ObjectX").ToList(); 232 | Assert.Equal(list1.Count, originalxCount + 1); 233 | o1 = connection.Get(guid); 234 | Assert.Equal(o1.ObjectXId, guid); 235 | o1.Name = "Bar"; 236 | connection.Update(o1); 237 | o1 = connection.Get(guid); 238 | Assert.Equal("Bar", o1.Name); 239 | connection.Delete(o1); 240 | o1 = connection.Get(guid); 241 | Assert.Null(o1); 242 | 243 | const int id = 42; 244 | var o2 = new ObjectY { ObjectYId = id, Name = "Foo" }; 245 | var originalyCount = connection.Query("Select Count(*) From ObjectY").First(); 246 | connection.Insert(o2); 247 | var list2 = connection.Query("select * from ObjectY").ToList(); 248 | Assert.Equal(list2.Count, originalyCount + 1); 249 | o2 = connection.Get(id); 250 | Assert.Equal(o2.ObjectYId, id); 251 | o2.Name = "Bar"; 252 | connection.Update(o2); 253 | o2 = connection.Get(id); 254 | Assert.Equal("Bar", o2.Name); 255 | connection.Delete(o2); 256 | o2 = connection.Get(id); 257 | Assert.Null(o2); 258 | } 259 | } 260 | 261 | [Fact] 262 | public void GetAllWithExplicitKey() 263 | { 264 | using (var connection = GetOpenConnection()) 265 | { 266 | var guid = Guid.NewGuid().ToString(); 267 | var o1 = new ObjectX { ObjectXId = guid, Name = "Foo" }; 268 | connection.Insert(o1); 269 | 270 | var objectXs = connection.GetAll().ToList(); 271 | Assert.True(objectXs.Count > 0); 272 | Assert.Equal(1, objectXs.Count(x => x.ObjectXId == guid)); 273 | } 274 | } 275 | 276 | [Fact] 277 | public void InsertGetUpdateDeleteWithExplicitKeyNamedId() 278 | { 279 | using (var connection = GetOpenConnection()) 280 | { 281 | const int id = 42; 282 | var o2 = new ObjectZ { Id = id, Name = "Foo" }; 283 | connection.Insert(o2); 284 | var list2 = connection.Query("select * from ObjectZ").ToList(); 285 | Assert.Single(list2); 286 | o2 = connection.Get(id); 287 | Assert.Equal(o2.Id, id); 288 | } 289 | } 290 | 291 | [Fact] 292 | public void ShortIdentity() 293 | { 294 | using (var connection = GetOpenConnection()) 295 | { 296 | const string name = "First item"; 297 | var id = connection.Insert(new Stuff { Name = name }); 298 | Assert.True(id > 0); // 1-n are valid here, due to parallel tests 299 | var item = connection.Get(id); 300 | Assert.Equal(item.TheId, (short)id); 301 | Assert.Equal(item.Name, name); 302 | } 303 | } 304 | 305 | [Fact] 306 | public void NullDateTime() 307 | { 308 | using (var connection = GetOpenConnection()) 309 | { 310 | connection.Insert(new Stuff { Name = "First item" }); 311 | connection.Insert(new Stuff { Name = "Second item", Created = DateTime.Now }); 312 | var stuff = connection.Query("select * from Stuff").ToList(); 313 | Assert.Null(stuff[0].Created); 314 | Assert.NotNull(stuff.Last().Created); 315 | } 316 | } 317 | 318 | [Fact] 319 | public void TableName() 320 | { 321 | using (var connection = GetOpenConnection()) 322 | { 323 | // tests against "Automobiles" table (Table attribute) 324 | var id = connection.Insert(new Car { Name = "Volvo" }); 325 | var car = connection.Get(id); 326 | Assert.NotNull(car); 327 | Assert.Equal("Volvo", car.Name); 328 | Assert.Equal("Volvo", connection.Get(id).Name); 329 | Assert.True(connection.Update(new Car { Id = (int)id, Name = "Saab" })); 330 | Assert.Equal("Saab", connection.Get(id).Name); 331 | Assert.True(connection.Delete(new Car { Id = (int)id })); 332 | Assert.Null(connection.Get(id)); 333 | } 334 | } 335 | 336 | [Fact] 337 | public void TestSimpleGet() 338 | { 339 | using (var connection = GetOpenConnection()) 340 | { 341 | var id = connection.Insert(new User { Name = "Adama", Age = 10 }); 342 | var user = connection.Get(id); 343 | Assert.Equal(user.Id, (int)id); 344 | Assert.Equal("Adama", user.Name); 345 | connection.Delete(user); 346 | } 347 | } 348 | 349 | [Fact] 350 | public void TestClosedConnection() 351 | { 352 | using (var connection = GetConnection()) 353 | { 354 | Assert.True(connection.Insert(new User { Name = "Adama", Age = 10 }) > 0); 355 | var users = connection.GetAll(); 356 | Assert.NotEmpty(users); 357 | } 358 | } 359 | 360 | [Fact] 361 | public void InsertEnumerable() 362 | { 363 | InsertHelper(src => src.AsEnumerable()); 364 | } 365 | 366 | [Fact] 367 | public void InsertArray() 368 | { 369 | InsertHelper(src => src.ToArray()); 370 | } 371 | 372 | [Fact] 373 | public void InsertList() 374 | { 375 | InsertHelper(src => src.ToList()); 376 | } 377 | 378 | private void InsertHelper(Func, T> helper) 379 | where T : class 380 | { 381 | const int numberOfEntities = 10; 382 | 383 | var users = new List(numberOfEntities); 384 | for (var i = 0; i < numberOfEntities; i++) 385 | users.Add(new User { Name = "User " + i, Age = i }); 386 | 387 | using (var connection = GetOpenConnection()) 388 | { 389 | connection.DeleteAll(); 390 | 391 | var total = connection.Insert(helper(users)); 392 | Assert.Equal(total, numberOfEntities); 393 | users = connection.Query("select * from Users").ToList(); 394 | Assert.Equal(users.Count, numberOfEntities); 395 | } 396 | } 397 | 398 | [Fact] 399 | public void UpdateEnumerable() 400 | { 401 | UpdateHelper(src => src.AsEnumerable()); 402 | } 403 | 404 | [Fact] 405 | public void UpdateArray() 406 | { 407 | UpdateHelper(src => src.ToArray()); 408 | } 409 | 410 | [Fact] 411 | public void UpdateList() 412 | { 413 | UpdateHelper(src => src.ToList()); 414 | } 415 | 416 | private void UpdateHelper(Func, T> helper) 417 | where T : class 418 | { 419 | const int numberOfEntities = 10; 420 | 421 | var users = new List(numberOfEntities); 422 | for (var i = 0; i < numberOfEntities; i++) 423 | users.Add(new User { Name = "User " + i, Age = i }); 424 | 425 | using (var connection = GetOpenConnection()) 426 | { 427 | connection.DeleteAll(); 428 | 429 | var total = connection.Insert(helper(users)); 430 | Assert.Equal(total, numberOfEntities); 431 | users = connection.Query("select * from Users").ToList(); 432 | Assert.Equal(users.Count, numberOfEntities); 433 | foreach (var user in users) 434 | { 435 | user.Name += " updated"; 436 | } 437 | connection.Update(helper(users)); 438 | var name = connection.Query("select * from Users").First().Name; 439 | Assert.Contains("updated", name); 440 | } 441 | } 442 | 443 | [Fact] 444 | public void DeleteEnumerable() 445 | { 446 | DeleteHelper(src => src.AsEnumerable()); 447 | } 448 | 449 | [Fact] 450 | public void DeleteArray() 451 | { 452 | DeleteHelper(src => src.ToArray()); 453 | } 454 | 455 | [Fact] 456 | public void DeleteList() 457 | { 458 | DeleteHelper(src => src.ToList()); 459 | } 460 | 461 | private void DeleteHelper(Func, T> helper) 462 | where T : class 463 | { 464 | const int numberOfEntities = 10; 465 | 466 | var users = new List(numberOfEntities); 467 | for (var i = 0; i < numberOfEntities; i++) 468 | users.Add(new User { Name = "User " + i, Age = i }); 469 | 470 | using (var connection = GetOpenConnection()) 471 | { 472 | connection.DeleteAll(); 473 | 474 | var total = connection.Insert(helper(users)); 475 | Assert.Equal(total, numberOfEntities); 476 | users = connection.Query("select * from Users").ToList(); 477 | Assert.Equal(users.Count, numberOfEntities); 478 | 479 | var usersToDelete = users.Take(10).ToList(); 480 | connection.Delete(helper(usersToDelete)); 481 | users = connection.Query("select * from Users").ToList(); 482 | Assert.Equal(users.Count, numberOfEntities - 10); 483 | } 484 | } 485 | 486 | [Fact] 487 | public void InsertGetUpdate() 488 | { 489 | using (var connection = GetOpenConnection()) 490 | { 491 | connection.DeleteAll(); 492 | Assert.Null(connection.Get(3)); 493 | 494 | //insert with computed attribute that should be ignored 495 | connection.Insert(new Car { Name = "Volvo", Computed = "this property should be ignored" }); 496 | 497 | var id = connection.Insert(new User { Name = "Adam", Age = 10 }); 498 | 499 | //get a user with "isdirty" tracking 500 | var user = connection.Get(id); 501 | Assert.Equal("Adam", user.Name); 502 | Assert.False(connection.Update(user)); //returns false if not updated, based on tracking 503 | user.Name = "Bob"; 504 | Assert.True(connection.Update(user)); //returns true if updated, based on tracking 505 | user = connection.Get(id); 506 | Assert.Equal("Bob", user.Name); 507 | 508 | //get a user with no tracking 509 | var notrackedUser = connection.Get(id); 510 | Assert.Equal("Bob", notrackedUser.Name); 511 | Assert.True(connection.Update(notrackedUser)); //returns true, even though user was not changed 512 | notrackedUser.Name = "Cecil"; 513 | Assert.True(connection.Update(notrackedUser)); 514 | Assert.Equal("Cecil", connection.Get(id).Name); 515 | 516 | Assert.Single(connection.Query("select * from Users")); 517 | Assert.True(connection.Delete(user)); 518 | Assert.Empty(connection.Query("select * from Users")); 519 | 520 | Assert.False(connection.Update(notrackedUser)); //returns false, user not found 521 | } 522 | } 523 | 524 | #if SQLCE 525 | [Fact(Skip = "Not parallel friendly - thinking about how to test this")] 526 | public void InsertWithCustomDbType() 527 | { 528 | SqlMapperExtensions.GetDatabaseType = conn => "SQLiteConnection"; 529 | 530 | bool sqliteCodeCalled = false; 531 | using (var connection = GetOpenConnection()) 532 | { 533 | connection.DeleteAll(); 534 | Assert.IsNull(connection.Get(3)); 535 | try 536 | { 537 | connection.Insert(new User { Name = "Adam", Age = 10 }); 538 | } 539 | catch (SqlCeException ex) 540 | { 541 | sqliteCodeCalled = ex.Message.IndexOf("There was an error parsing the query", StringComparison.OrdinalIgnoreCase) >= 0; 542 | } 543 | // ReSharper disable once EmptyGeneralCatchClause 544 | catch (Exception) 545 | { 546 | } 547 | } 548 | SqlMapperExtensions.GetDatabaseType = null; 549 | 550 | if (!sqliteCodeCalled) 551 | { 552 | throw new Exception("Was expecting sqlite code to be called"); 553 | } 554 | } 555 | #endif 556 | 557 | [Fact] 558 | public void InsertWithCustomTableNameMapper() 559 | { 560 | SqlMapperExtensions.TableNameMapper = type => 561 | { 562 | switch (type.Name) 563 | { 564 | case "Person": 565 | return "People"; 566 | default: 567 | var tableattr = type.GetCustomAttributes(false).SingleOrDefault(attr => attr.GetType().Name == "TableAttribute") as dynamic; 568 | if (tableattr != null) 569 | return tableattr.Name; 570 | 571 | var name = type.Name + "s"; 572 | if (type.IsInterface && name.StartsWith("I")) 573 | return name.Substring(1); 574 | return name; 575 | } 576 | }; 577 | 578 | using (var connection = GetOpenConnection()) 579 | { 580 | var id = connection.Insert(new Person { Name = "Mr Mapper" }); 581 | Assert.Equal(1, id); 582 | connection.GetAll(); 583 | } 584 | } 585 | 586 | [Fact] 587 | public void GetAll() 588 | { 589 | const int numberOfEntities = 10; 590 | 591 | var users = new List(numberOfEntities); 592 | for (var i = 0; i < numberOfEntities; i++) 593 | users.Add(new User { Name = "User " + i, Age = i }); 594 | 595 | using (var connection = GetOpenConnection()) 596 | { 597 | connection.DeleteAll(); 598 | 599 | var total = connection.Insert(users); 600 | Assert.Equal(total, numberOfEntities); 601 | users = connection.GetAll().ToList(); 602 | Assert.Equal(users.Count, numberOfEntities); 603 | var iusers = connection.GetAll().ToList(); 604 | Assert.Equal(iusers.Count, numberOfEntities); 605 | for (var i = 0; i < numberOfEntities; i++) 606 | Assert.Equal(iusers[i].Age, i); 607 | } 608 | } 609 | 610 | /// 611 | /// Test for issue #933 612 | /// 613 | [Fact] 614 | public void GetAndGetAllWithNullableValues() 615 | { 616 | using (var connection = GetOpenConnection()) 617 | { 618 | var id1 = connection.Insert(new NullableDate { DateValue = new DateTime(2011, 07, 14) }); 619 | var id2 = connection.Insert(new NullableDate { DateValue = null }); 620 | 621 | var value1 = connection.Get(id1); 622 | Assert.Equal(new DateTime(2011, 07, 14), value1.DateValue.Value); 623 | 624 | var value2 = connection.Get(id2); 625 | Assert.True(value2.DateValue == null); 626 | 627 | var value3 = connection.GetAll().ToList(); 628 | Assert.Equal(new DateTime(2011, 07, 14), value3[0].DateValue.Value); 629 | Assert.True(value3[1].DateValue == null); 630 | } 631 | } 632 | 633 | [Fact] 634 | public void Transactions() 635 | { 636 | using (var connection = GetOpenConnection()) 637 | { 638 | var id = connection.Insert(new Car { Name = "one car" }); //insert outside transaction 639 | 640 | var tran = connection.BeginTransaction(); 641 | var car = connection.Get(id, tran); 642 | var orgName = car.Name; 643 | car.Name = "Another car"; 644 | connection.Update(car, tran); 645 | tran.Rollback(); 646 | 647 | car = connection.Get(id); //updates should have been rolled back 648 | Assert.Equal(car.Name, orgName); 649 | } 650 | } 651 | #if TRANSCOPE 652 | [Fact] 653 | public void TransactionScope() 654 | { 655 | using (var txscope = new TransactionScope()) 656 | { 657 | using (var connection = GetOpenConnection()) 658 | { 659 | var id = connection.Insert(new Car { Name = "one car" }); //inser car within transaction 660 | 661 | txscope.Dispose(); //rollback 662 | 663 | Assert.Null(connection.Get(id)); //returns null - car with that id should not exist 664 | } 665 | } 666 | } 667 | #endif 668 | 669 | [Fact] 670 | public void InsertCheckKey() 671 | { 672 | using (var connection = GetOpenConnection()) 673 | { 674 | Assert.Null(connection.Get(3)); 675 | User user = new User { Name = "Adamb", Age = 10 }; 676 | int id = (int)connection.Insert(user); 677 | Assert.Equal(user.Id, id); 678 | } 679 | } 680 | 681 | [Fact] 682 | public void BuilderSelectClause() 683 | { 684 | using (var connection = GetOpenConnection()) 685 | { 686 | var rand = new Random(8675309); 687 | var data = new List(100); 688 | for (int i = 0; i < 100; i++) 689 | { 690 | var nU = new User { Age = rand.Next(70), Id = i, Name = Guid.NewGuid().ToString() }; 691 | data.Add(nU); 692 | nU.Id = (int)connection.Insert(nU); 693 | } 694 | 695 | var builder = new SqlBuilder(); 696 | var justId = builder.AddTemplate("SELECT /**select**/ FROM Users"); 697 | var all = builder.AddTemplate("SELECT Name, /**select**/, Age FROM Users"); 698 | 699 | builder.Select("Id"); 700 | 701 | var ids = connection.Query(justId.RawSql, justId.Parameters); 702 | var users = connection.Query(all.RawSql, all.Parameters); 703 | 704 | foreach (var u in data) 705 | { 706 | if (!ids.Any(i => u.Id == i)) throw new Exception("Missing ids in select"); 707 | if (!users.Any(a => a.Id == u.Id && a.Name == u.Name && a.Age == u.Age)) throw new Exception("Missing users in select"); 708 | } 709 | } 710 | } 711 | 712 | [Fact] 713 | public void BuilderTemplateWithoutComposition() 714 | { 715 | var builder = new SqlBuilder(); 716 | var template = builder.AddTemplate("SELECT COUNT(*) FROM Users WHERE Age = @age", new { age = 5 }); 717 | 718 | if (template.RawSql == null) throw new Exception("RawSql null"); 719 | if (template.Parameters == null) throw new Exception("Parameters null"); 720 | 721 | using (var connection = GetOpenConnection()) 722 | { 723 | connection.DeleteAll(); 724 | connection.Insert(new User { Age = 5, Name = "Testy McTestington" }); 725 | 726 | if (connection.Query(template.RawSql, template.Parameters).Single() != 1) 727 | throw new Exception("Query failed"); 728 | } 729 | } 730 | 731 | [Fact] 732 | public void InsertFieldWithReservedName() 733 | { 734 | using (var connection = GetOpenConnection()) 735 | { 736 | connection.DeleteAll(); 737 | var id = connection.Insert(new Result() { Name = "Adam", Order = 1 }); 738 | 739 | var result = connection.Get(id); 740 | Assert.Equal(1, result.Order); 741 | } 742 | } 743 | 744 | [Fact] 745 | public void DeleteAll() 746 | { 747 | using (var connection = GetOpenConnection()) 748 | { 749 | var id1 = connection.Insert(new User { Name = "Alice", Age = 32 }); 750 | var id2 = connection.Insert(new User { Name = "Bob", Age = 33 }); 751 | Assert.True(connection.DeleteAll()); 752 | Assert.Null(connection.Get(id1)); 753 | Assert.Null(connection.Get(id2)); 754 | } 755 | } 756 | } 757 | } 758 | -------------------------------------------------------------------------------- /tests/Dapper.Tests.Contrib/TestSuites.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.Sqlite; 2 | using MySqlConnector; 3 | using System; 4 | using System.Data; 5 | using System.Data.SqlClient; 6 | using System.IO; 7 | using Xunit; 8 | using Xunit.Sdk; 9 | 10 | namespace Dapper.Tests.Contrib 11 | { 12 | // The test suites here implement TestSuiteBase so that each provider runs 13 | // the entire set of tests without declarations per method 14 | // If we want to support a new provider, they need only be added here - not in multiple places 15 | 16 | [XunitTestCaseDiscoverer("Dapper.Tests.SkippableFactDiscoverer", "Dapper.Tests.Contrib")] 17 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 18 | public class SkippableFactAttribute : FactAttribute 19 | { 20 | } 21 | 22 | public class SqlServerTestSuite : TestSuite 23 | { 24 | private const string DbName = "tempdb"; 25 | public static string ConnectionString => 26 | GetConnectionString("SqlServerConnectionString", $"Data Source=.;Initial Catalog={DbName};Integrated Security=True"); 27 | 28 | public override IDbConnection GetConnection() => new SqlConnection(ConnectionString); 29 | 30 | static SqlServerTestSuite() 31 | { 32 | using (var connection = new SqlConnection(ConnectionString)) 33 | { 34 | // ReSharper disable once AccessToDisposedClosure 35 | void dropTable(string name) => connection.Execute($"IF OBJECT_ID('{name}', 'U') IS NOT NULL DROP TABLE [{name}]; "); 36 | connection.Open(); 37 | dropTable("Stuff"); 38 | connection.Execute("CREATE TABLE Stuff (TheId int IDENTITY(1,1) not null, Name nvarchar(100) not null, Created DateTime null);"); 39 | dropTable("People"); 40 | connection.Execute("CREATE TABLE People (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null);"); 41 | dropTable("Users"); 42 | connection.Execute("CREATE TABLE Users (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, Age int not null);"); 43 | dropTable("Automobiles"); 44 | connection.Execute("CREATE TABLE Automobiles (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null);"); 45 | dropTable("Results"); 46 | connection.Execute("CREATE TABLE Results (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, [Order] int not null);"); 47 | dropTable("ObjectX"); 48 | connection.Execute("CREATE TABLE ObjectX (ObjectXId nvarchar(100) not null, Name nvarchar(100) not null);"); 49 | dropTable("ObjectY"); 50 | connection.Execute("CREATE TABLE ObjectY (ObjectYId int not null, Name nvarchar(100) not null);"); 51 | dropTable("ObjectZ"); 52 | connection.Execute("CREATE TABLE ObjectZ (Id int not null, Name nvarchar(100) not null);"); 53 | dropTable("GenericType"); 54 | connection.Execute("CREATE TABLE GenericType (Id nvarchar(100) not null, Name nvarchar(100) not null);"); 55 | dropTable("NullableDates"); 56 | connection.Execute("CREATE TABLE NullableDates (Id int IDENTITY(1,1) not null, DateValue DateTime null);"); 57 | } 58 | } 59 | } 60 | 61 | public class MySqlServerTestSuite : TestSuite 62 | { 63 | public static string ConnectionString { get; } = 64 | GetConnectionString("MySqlConnectionString", "Server=localhost;Database=tests;Uid=test;Pwd=pass;UseAffectedRows=false;"); 65 | 66 | public override IDbConnection GetConnection() 67 | { 68 | if (_skip) Skip.Inconclusive("Skipping MySQL Tests - no server."); 69 | return new MySqlConnection(ConnectionString); 70 | } 71 | 72 | private static readonly bool _skip; 73 | 74 | static MySqlServerTestSuite() 75 | { 76 | try 77 | { 78 | using (var connection = new MySqlConnection(ConnectionString)) 79 | { 80 | // ReSharper disable once AccessToDisposedClosure 81 | void dropTable(string name) => connection.Execute($"DROP TABLE IF EXISTS `{name}`;"); 82 | connection.Open(); 83 | dropTable("Stuff"); 84 | connection.Execute("CREATE TABLE Stuff (TheId int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null, Created DateTime null);"); 85 | dropTable("People"); 86 | connection.Execute("CREATE TABLE People (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null);"); 87 | dropTable("Users"); 88 | connection.Execute("CREATE TABLE Users (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null, Age int not null);"); 89 | dropTable("Automobiles"); 90 | connection.Execute("CREATE TABLE Automobiles (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null);"); 91 | dropTable("Results"); 92 | connection.Execute("CREATE TABLE Results (Id int not null AUTO_INCREMENT PRIMARY KEY, Name nvarchar(100) not null, `Order` int not null);"); 93 | dropTable("ObjectX"); 94 | connection.Execute("CREATE TABLE ObjectX (ObjectXId nvarchar(100) not null, Name nvarchar(100) not null);"); 95 | dropTable("ObjectY"); 96 | connection.Execute("CREATE TABLE ObjectY (ObjectYId int not null, Name nvarchar(100) not null);"); 97 | dropTable("ObjectZ"); 98 | connection.Execute("CREATE TABLE ObjectZ (Id int not null, Name nvarchar(100) not null);"); 99 | dropTable("GenericType"); 100 | connection.Execute("CREATE TABLE GenericType (Id nvarchar(100) not null, Name nvarchar(100) not null);"); 101 | dropTable("NullableDates"); 102 | connection.Execute("CREATE TABLE NullableDates (Id int not null AUTO_INCREMENT PRIMARY KEY, DateValue DateTime);"); 103 | } 104 | } 105 | catch (MySqlException e) 106 | { 107 | if (e.Message.Contains("Unable to connect")) 108 | _skip = true; 109 | else 110 | throw; 111 | } 112 | } 113 | } 114 | 115 | public class SQLiteTestSuite : TestSuite 116 | { 117 | private const string FileName = "Test.DB.sqlite"; 118 | public static string ConnectionString => $"Filename=./{FileName};Mode=ReadWriteCreate;"; 119 | public override IDbConnection GetConnection() => new SqliteConnection(ConnectionString); 120 | 121 | static SQLiteTestSuite() 122 | { 123 | if (File.Exists(FileName)) 124 | { 125 | File.Delete(FileName); 126 | } 127 | using (var connection = new SqliteConnection(ConnectionString)) 128 | { 129 | connection.Open(); 130 | connection.Execute("CREATE TABLE Stuff (TheId integer primary key autoincrement not null, Name nvarchar(100) not null, Created DateTime null) "); 131 | connection.Execute("CREATE TABLE People (Id integer primary key autoincrement not null, Name nvarchar(100) not null) "); 132 | connection.Execute("CREATE TABLE Users (Id integer primary key autoincrement not null, Name nvarchar(100) not null, Age int not null) "); 133 | connection.Execute("CREATE TABLE Automobiles (Id integer primary key autoincrement not null, Name nvarchar(100) not null) "); 134 | connection.Execute("CREATE TABLE Results (Id integer primary key autoincrement not null, Name nvarchar(100) not null, [Order] int not null) "); 135 | connection.Execute("CREATE TABLE ObjectX (ObjectXId nvarchar(100) not null, Name nvarchar(100) not null) "); 136 | connection.Execute("CREATE TABLE ObjectY (ObjectYId integer not null, Name nvarchar(100) not null) "); 137 | connection.Execute("CREATE TABLE ObjectZ (Id integer not null, Name nvarchar(100) not null) "); 138 | connection.Execute("CREATE TABLE GenericType (Id nvarchar(100) not null, Name nvarchar(100) not null) "); 139 | connection.Execute("CREATE TABLE NullableDates (Id integer primary key autoincrement not null, DateValue DateTime) "); 140 | } 141 | } 142 | } 143 | 144 | 145 | #if SQLCE 146 | public class SqlCETestSuite : TestSuite 147 | { 148 | const string FileName = "Test.DB.sdf"; 149 | public static string ConnectionString => $"Data Source={FileName};"; 150 | public override IDbConnection GetConnection() => new SqlCeConnection(ConnectionString); 151 | 152 | static SqlCETestSuite() 153 | { 154 | if (File.Exists(FileName)) 155 | { 156 | File.Delete(FileName); 157 | } 158 | var engine = new SqlCeEngine(ConnectionString); 159 | engine.CreateDatabase(); 160 | using (var connection = new SqlCeConnection(ConnectionString)) 161 | { 162 | connection.Open(); 163 | connection.Execute(@"CREATE TABLE Stuff (TheId int IDENTITY(1,1) not null, Name nvarchar(100) not null, Created DateTime null) "); 164 | connection.Execute(@"CREATE TABLE People (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null) "); 165 | connection.Execute(@"CREATE TABLE Users (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, Age int not null) "); 166 | connection.Execute(@"CREATE TABLE Automobiles (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null) "); 167 | connection.Execute(@"CREATE TABLE Results (Id int IDENTITY(1,1) not null, Name nvarchar(100) not null, [Order] int not null) "); 168 | connection.Execute(@"CREATE TABLE ObjectX (ObjectXId nvarchar(100) not null, Name nvarchar(100) not null) "); 169 | connection.Execute(@"CREATE TABLE ObjectY (ObjectYId int not null, Name nvarchar(100) not null) "); 170 | connection.Execute(@"CREATE TABLE ObjectZ (Id int not null, Name nvarchar(100) not null) "); 171 | connection.Execute(@"CREATE TABLE GenericType (Id nvarchar(100) not null, Name nvarchar(100) not null) "); 172 | connection.Execute(@"CREATE TABLE NullableDates (Id int IDENTITY(1,1) not null, DateValue DateTime null) "); 173 | } 174 | Console.WriteLine("Created database"); 175 | } 176 | } 177 | #endif 178 | } 179 | -------------------------------------------------------------------------------- /tests/Dapper.Tests.Contrib/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "shadowCopy": false 4 | } -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | false 6 | false 7 | false 8 | false 9 | true 10 | 11 | Full 12 | $(DefineConstants);WINDOWS 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | -------------------------------------------------------------------------------- /tests/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mysql: 4 | image: mysql:8 5 | container_name: mysql 6 | ports: 7 | - 3306:3306 8 | environment: 9 | MYSQL_ROOT_PASSWORD: root 10 | MYSQL_DATABASE: test 11 | postgres: 12 | image: postgres:alpine 13 | container_name: postgres 14 | ports: 15 | - 5432:5432 16 | environment: 17 | POSTGRES_USER: postgres 18 | POSTGRES_PASSWORD: postgres 19 | POSTGRES_DB: test 20 | sqlserver: 21 | image: mcr.microsoft.com/mssql/server:2019-latest 22 | container_name: sql-server-db 23 | ports: 24 | - 1433:1433 25 | environment: 26 | ACCEPT_EULA: Y 27 | SA_PASSWORD: "Password." 28 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "assemblyVersion": "2.0.0.0", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/main$", 6 | "^refs/tags/v\\d+\\.\\d+" 7 | ], 8 | "nugetPackageVersion": { 9 | "semVer": 2 10 | }, 11 | "cloudBuild": { 12 | "buildNumber": { 13 | "enabled": true, 14 | "setVersionVariables": true 15 | } 16 | } 17 | } --------------------------------------------------------------------------------