├── .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 | [](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/) | [](https://www.nuget.org/packages/Dapper.Contrib/) | [](https://www.nuget.org/packages/Dapper.Contrib/) | [](https://www.nuget.org/packages/Dapper.Contrib/) | [](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