├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── dotnet.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── N.EntityFramework.Extensions.Test ├── Data │ ├── Enums │ │ └── ProductStatus.cs │ ├── Order.cs │ ├── Product.cs │ ├── ProductCategory.cs │ ├── ProductWithComplexKeys.cs │ ├── ProductWithCustomSchema.cs │ ├── ProductWithTrigger.cs │ ├── TestDbConfiguration.cs │ ├── TestDbContext.cs │ ├── TpcCustomer.cs │ ├── TpcPerson.cs │ ├── TpcVendor.cs │ ├── TphCustomer.cs │ ├── TphPerson.cs │ └── TphVendor.cs ├── DatabaseExtensions │ ├── DatabaseExtensionsBase.cs │ ├── SqlQueryToCsvFile.cs │ ├── SqlQueryToCsvFileAsync.cs │ ├── SqlQuery_Count.cs │ ├── SqlQuery_CountAsync.cs │ ├── TableExists.cs │ ├── TruncateTable.cs │ └── TruncateTableAsync.cs ├── DbContextExtensions │ ├── BulkDelete.cs │ ├── BulkDeleteAsync.cs │ ├── BulkFetch.cs │ ├── BulkInsert.cs │ ├── BulkInsertAsync.cs │ ├── BulkMerge.cs │ ├── BulkMergeAsync.cs │ ├── BulkSaveChanges.cs │ ├── BulkSaveChangesAsync.cs │ ├── BulkSync.cs │ ├── BulkSyncAsync.cs │ ├── BulkUpdate.cs │ ├── BulkUpdateAsync.cs │ ├── DbContextExtensionsBase.cs │ ├── DeleteFromQuery.cs │ ├── DeleteFromQueryAsync.cs │ ├── Fetch.cs │ ├── FetchAsync.cs │ ├── InsertFromQuery.cs │ ├── InsertFromQueryAsync.cs │ ├── QueryToCsvFile.cs │ ├── QueryToCsvFileAsync.cs │ ├── UpdateFromQuery.cs │ └── UpdateFromQueryAsync.cs ├── DbSetExtensions │ ├── Clear.cs │ ├── ClearAsync.cs │ ├── Truncate.cs │ └── TruncateAsync.cs ├── Migrations │ ├── 202406080146394_Initial.Designer.cs │ ├── 202406080146394_Initial.cs │ └── 202406080146394_Initial.resx ├── N.EntityFramework.Extensions.Test.csproj └── Properties │ └── AssemblyInfo.cs ├── N.EntityFramework.Extensions.sln ├── N.EntityFramework.Extensions ├── Common │ └── Constants.cs ├── Data │ ├── BulkDeleteOptions.cs │ ├── BulkFetchOptions.cs │ ├── BulkInsertOptions.cs │ ├── BulkInsertResult.cs │ ├── BulkMergeOption.cs │ ├── BulkMergeOutputRow.cs │ ├── BulkMergeResult.cs │ ├── BulkOptions.cs │ ├── BulkQueryResult.cs │ ├── BulkSyncOptions.cs │ ├── BulkSyncResult.cs │ ├── BulkUpdateOptions.cs │ ├── DatabaseExtensions.cs │ ├── DatabaseExtensionsAsync.cs │ ├── DbContextExtensions.cs │ ├── DbContextExtensionsAsync.cs │ ├── DbTransactionContext.cs │ ├── EfExtensionsCommand.cs │ ├── EfExtensionsCommandInterceptor.cs │ ├── EntityDataReader.cs │ ├── FetchOptions.cs │ ├── FetchResult.cs │ ├── QueryToFileOptions.cs │ ├── QueryToFileResult.cs │ ├── SqlMergeAction.cs │ ├── SqlQuery.cs │ └── TableMapping.cs ├── Enums │ ├── ConnectionBehavior.cs │ └── EfExtensionsCommandType.cs ├── Extensions │ ├── CommonExtensions.cs │ ├── LinqExtensions.cs │ └── ObjectExtensions.cs ├── N.EntityFramework.Extensions.csproj ├── Sql │ ├── SqlBuilder.cs │ └── SqlClause.cs └── Util │ ├── CommonUtil.cs │ ├── SqlClientUtil.cs │ └── SqlUtil.cs └── README.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v2 18 | with: 19 | dotnet-version: 6.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Nuget personal access tokens and Credentials 210 | nuget.config 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio LightSwitch build output 301 | **/*.HTMLClient/GeneratedArtifacts 302 | **/*.DesktopClient/GeneratedArtifacts 303 | **/*.DesktopClient/ModelManifest.xml 304 | **/*.Server/GeneratedArtifacts 305 | **/*.Server/ModelManifest.xml 306 | _Pvt_Extensions 307 | 308 | # Paket dependency manager 309 | .paket/paket.exe 310 | paket-files/ 311 | 312 | # FAKE - F# Make 313 | .fake/ 314 | 315 | # CodeRush personal settings 316 | .cr/personal 317 | 318 | # Python Tools for Visual Studio (PTVS) 319 | __pycache__/ 320 | *.pyc 321 | 322 | # Cake - Uncomment if you are using it 323 | # tools/** 324 | # !tools/packages.config 325 | 326 | # Tabs Studio 327 | *.tss 328 | 329 | # Telerik's JustMock configuration file 330 | *.jmconfig 331 | 332 | # BizTalk build output 333 | *.btp.cs 334 | *.btm.cs 335 | *.odx.cs 336 | *.xsd.cs 337 | 338 | # OpenCover UI analysis results 339 | OpenCover/ 340 | 341 | # Azure Stream Analytics local run output 342 | ASALocalRun/ 343 | 344 | # MSBuild Binary and Structured Log 345 | *.binlog 346 | 347 | # NVidia Nsight GPU debugger configuration file 348 | *.nvuser 349 | 350 | # MFractors (Xamarin productivity tool) working folder 351 | .mfractor/ 352 | 353 | # Local History for Visual Studio 354 | .localhistory/ 355 | 356 | # BeatPulse healthcheck temp database 357 | healthchecksdb 358 | 359 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 360 | MigrationBackup/ 361 | 362 | # Ionide (cross platform F# VS Code tools) working folder 363 | .ionide/ 364 | 365 | # Fody - auto-generated XML schema 366 | FodyWeavers.xsd 367 | 368 | # VS Code files for those working on multiple tools 369 | .vscode/* 370 | !.vscode/settings.json 371 | !.vscode/tasks.json 372 | !.vscode/launch.json 373 | !.vscode/extensions.json 374 | *.code-workspace 375 | 376 | # Local History for Visual Studio Code 377 | .history/ 378 | 379 | # Windows Installer files from build outputs 380 | *.cab 381 | *.msi 382 | *.msix 383 | *.msm 384 | *.msp 385 | 386 | # JetBrains Rider 387 | .idea/ 388 | *.sln.iml -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | NorthernLight25@outlook.com.. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NorthernLight1 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/Enums/ProductStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions.Test.Data.Enums; 8 | 9 | public enum ProductStatus 10 | { 11 | InStock, 12 | OutOfStock, 13 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace N.EntityFramework.Extensions.Test.Data; 5 | 6 | public class Order 7 | { 8 | [Key] 9 | public long Id { get; set; } 10 | public string ExternalId { get; set; } 11 | public Guid? GlobalId { get; set; } 12 | public decimal Price { get; set; } 13 | public DateTime AddedDateTime { get; set; } 14 | public DateTime? ModifiedDateTime { get; set; } 15 | public DateTimeOffset? ModifiedDateTimeOffset { get; set; } 16 | public bool? Trigger { get; set; } 17 | public bool Active { get; set; } 18 | public Order() 19 | { 20 | AddedDateTime = DateTime.UtcNow; 21 | Active = true; 22 | } 23 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/Product.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Text; 6 | using N.EntityFramework.Extensions.Test.Data.Enums; 7 | 8 | namespace N.EntityFramework.Extensions.Test.Data; 9 | 10 | public class Product 11 | { 12 | [Key] 13 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 14 | public string Id { get; set; } 15 | public decimal Price { get; set; } 16 | public bool OutOfStock { get; set; } 17 | [Column("Status")] 18 | [StringLength(25)] 19 | public string StatusString { get; set; } 20 | public int? ProductCategoryId { get; set; } 21 | public ProductStatus? StatusEnum { get; set; } 22 | public DateTime? UpdatedDateTime { get; set; } 23 | [Timestamp] 24 | public byte[] RowVersion { get; set; } 25 | 26 | public virtual ProductCategory ProductCategory { get; set; } 27 | public Product() 28 | { 29 | 30 | } 31 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/ProductCategory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions.Test.Data; 8 | 9 | public class ProductCategory 10 | { 11 | public int Id { get; set; } 12 | public string Name { get; set; } 13 | public bool Active { get; internal set; } 14 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/ProductWithComplexKeys.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Text; 6 | 7 | namespace N.EntityFramework.Extensions.Test.Data; 8 | 9 | public class ProductWithComplexKey 10 | { 11 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 12 | [Key, Column(Order = 1)] 13 | public Guid Key1 { get; set; } 14 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 15 | [Key, Column(Order = 2)] 16 | public Guid Key2 { get; set; } 17 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 18 | [Key, Column(Order = 3)] 19 | public Guid Key3 { get; set; } 20 | public decimal Price { get; set; } 21 | public bool OutOfStock { get; set; } 22 | [Column("Status")] 23 | [StringLength(25)] 24 | public string StatusString { get; set; } 25 | public DateTime? UpdatedDateTime { get; set; } 26 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/ProductWithCustomSchema.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace N.EntityFramework.Extensions.Test.Data; 10 | 11 | public class ProductWithCustomSchema 12 | { 13 | [Key] 14 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 15 | public string Id { get; set; } 16 | [StringLength(50)] 17 | public string Name { get; set; } 18 | public decimal Price { get; set; } 19 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/ProductWithTrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Text; 6 | using N.EntityFramework.Extensions.Test.Data.Enums; 7 | 8 | namespace N.EntityFramework.Extensions.Test.Data; 9 | 10 | public class ProductWithTrigger 11 | { 12 | [Key] 13 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 14 | public string Id { get; set; } 15 | public decimal Price { get; set; } 16 | public bool OutOfStock { get; set; } 17 | [Column("Status")] 18 | [StringLength(25)] 19 | public string StatusString { get; set; } 20 | public int? ProductCategoryId { get; set; } 21 | public ProductStatus? StatusEnum { get; set; } 22 | public DateTime? UpdatedDateTime { get; set; } 23 | [Timestamp] 24 | public byte[] RowVersion { get; set; } 25 | 26 | public virtual ProductCategory ProductCategory { get; set; } 27 | public ProductWithTrigger() 28 | { 29 | 30 | } 31 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/TestDbConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Entity.Migrations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace N.EntityFramework.Extensions.Test.Data; 9 | 10 | internal sealed class TestDbConfiguration : DbMigrationsConfiguration 11 | { 12 | public TestDbConfiguration() 13 | { 14 | AutomaticMigrationsEnabled = true; 15 | AutomaticMigrationDataLossAllowed = true; 16 | ContextKey = "N.EntityFramework.Extensions.Test"; 17 | } 18 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Entity; 2 | 3 | namespace N.EntityFramework.Extensions.Test.Data; 4 | 5 | public class TestDbContext : DbContext 6 | { 7 | private static readonly string _connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;Initial Catalog = N.EntityFramework.Extensions.Test; Integrated Security = True; MultipleActiveResultSets=True"; 8 | public virtual DbSet Orders { get; set; } 9 | public virtual DbSet Products { get; set; } 10 | public virtual DbSet ProductCategories { get; set; } 11 | public virtual DbSet ProductsWithComplexKey { get; set; } 12 | public virtual DbSet ProductsWithTrigger { get; set; } 13 | public virtual DbSet ProductsWithCustomSchema { get; set; } 14 | public virtual DbSet TpcPeople { get; set; } 15 | public virtual DbSet TphPeople { get; set; } 16 | public virtual DbSet TphCustomers { get; set; } 17 | public virtual DbSet TphVendors { get; set; } 18 | 19 | public TestDbContext() : base(_connectionString) 20 | { 21 | Database.SetInitializer(new MigrateDatabaseToLatestVersion()); 22 | } 23 | protected override void OnModelCreating(DbModelBuilder modelBuilder) 24 | { 25 | modelBuilder.Entity().ToTable(nameof(ProductsWithCustomSchema), "top"); 26 | 27 | modelBuilder.Entity() 28 | .Property(s => s.Price).HasPrecision(8, 2); 29 | //modelBuilder.Entity() 30 | // .Property(s => s.DbAddedDateTime) 31 | // .HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Computed); 32 | modelBuilder.Entity().Map(m => 33 | { 34 | m.MapInheritedProperties(); 35 | m.ToTable("TpcCustomer"); 36 | }); 37 | modelBuilder.Entity().Map(m => 38 | { 39 | m.MapInheritedProperties(); 40 | m.ToTable("TpcVendor"); 41 | }); 42 | base.OnModelCreating(modelBuilder); 43 | } 44 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/TpcCustomer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace N.EntityFramework.Extensions.Test.Data; 9 | 10 | public class TpcCustomer : TpcPerson 11 | { 12 | public string Email { get; set; } 13 | public string Phone { get; set; } 14 | public DateTime AddedDate { get; set; } 15 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/TpcPerson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace N.EntityFramework.Extensions.Test.Data; 9 | 10 | public abstract class TpcPerson 11 | { 12 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 13 | public long Id { get; set; } 14 | public string FirstName { get; set; } 15 | public string LastName { get; set; } 16 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/TpcVendor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace N.EntityFramework.Extensions.Test.Data; 9 | 10 | public class TpcVendor : TpcPerson 11 | { 12 | public string Email { get; set; } 13 | public string Phone { get; set; } 14 | public string Url { get; set; } 15 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/TphCustomer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions.Test.Data; 8 | 9 | public class TphCustomer : TphPerson 10 | { 11 | public string Email { get; set; } 12 | public string Phone { get; set; } 13 | public DateTime AddedDate { get; set; } 14 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/TphPerson.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace N.EntityFramework.Extensions.Test.Data; 9 | 10 | [Table("TphPeople")] 11 | public abstract class TphPerson 12 | { 13 | public long Id { get; set; } 14 | public string FirstName { get; set; } 15 | public string LastName { get; set; } 16 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Data/TphVendor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions.Test.Data; 8 | 9 | public class TphVendor : TphPerson 10 | { 11 | public string Email { get; set; } 12 | public string Phone { get; set; } 13 | public string Url { get; set; } 14 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DatabaseExtensions/DatabaseExtensionsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using N.EntityFramework.Extensions.Test.Data; 8 | 9 | namespace N.EntityFramework.Extensions.Test.DatabaseExtensions; 10 | 11 | public class DatabaseExtensionsBase 12 | { 13 | protected static TestDbContext SetupDbContext(bool populateData) 14 | { 15 | var dbContext = new TestDbContext(); 16 | dbContext.Database.CreateIfNotExists(); 17 | dbContext.Orders.Truncate(); 18 | if (populateData) 19 | { 20 | var orders = new List(); 21 | int id = 1; 22 | for (int i = 0; i < 2050; i++) 23 | { 24 | DateTime addedDateTime = DateTime.UtcNow.AddDays(-id); 25 | orders.Add(new Order 26 | { 27 | Id = id, 28 | ExternalId = string.Format("id-{0}", i), 29 | Price = 1.25M, 30 | AddedDateTime = addedDateTime, 31 | ModifiedDateTime = addedDateTime.AddHours(3) 32 | }); 33 | id++; 34 | } 35 | for (int i = 0; i < 1050; i++) 36 | { 37 | orders.Add(new Order { Id = id, Price = 5.35M }); 38 | id++; 39 | } 40 | for (int i = 0; i < 2050; i++) 41 | { 42 | orders.Add(new Order { Id = id, Price = 1.25M }); 43 | id++; 44 | } 45 | for (int i = 0; i < 6000; i++) 46 | { 47 | orders.Add(new Order { Id = id, Price = 15.35M }); 48 | id++; 49 | } 50 | for (int i = 0; i < 6000; i++) 51 | { 52 | orders.Add(new Order { Id = id, Price = 15.35M }); 53 | id++; 54 | } 55 | 56 | Debug.WriteLine("Last Id for Order is {0}", id); 57 | dbContext.BulkInsert(orders, new BulkInsertOptions() { KeepIdentity = true }); 58 | } 59 | return dbContext; 60 | } 61 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DatabaseExtensions/SqlQueryToCsvFile.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SqlClient; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace N.EntityFramework.Extensions.Test.DatabaseExtensions; 6 | 7 | [TestClass] 8 | public class SqlQueryToCsvFile : DatabaseExtensionsBase 9 | { 10 | [TestMethod] 11 | public void With_Default_Options() 12 | { 13 | var dbContext = SetupDbContext(true); 14 | int count = dbContext.Orders.Where(o => o.Price > 5M).Count(); 15 | var queryToCsvFileResult = dbContext.Database.SqlQueryToCsvFile("SqlQueryToCsvFile-Test.csv", "SELECT * FROM Orders WHERE Price > @Price", new SqlParameter("@Price", 5M)); 16 | 17 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 18 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 19 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count + 1, "The total number of rows written to the file should match the count from the database plus the header row"); 20 | } 21 | [TestMethod] 22 | public void With_Options_ColumnDelimiter_TextQualifer() 23 | { 24 | var dbContext = SetupDbContext(true); 25 | string filePath = "SqlQueryToCsvFile_Options_ColumnDelimiter_TextQualifer-Test.csv"; 26 | int count = dbContext.Orders.Where(o => o.Price > 5M).Count(); 27 | var queryToCsvFileResult = dbContext.Database.SqlQueryToCsvFile(filePath, options => { options.ColumnDelimiter = "|"; options.TextQualifer = "\""; }, 28 | "SELECT * FROM Orders WHERE Price > @Price", new SqlParameter("@Price", 5M)); 29 | 30 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 31 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 32 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count + 1, "The total number of rows written to the file should match the count from the database plus the header row"); 33 | } 34 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DatabaseExtensions/SqlQueryToCsvFileAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SqlClient; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DatabaseExtensions; 7 | 8 | [TestClass] 9 | public class SqlQueryToCsvFileAsync : DatabaseExtensionsBase 10 | { 11 | [TestMethod] 12 | public async Task With_Default_Options() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | int count = dbContext.Orders.Where(o => o.Price > 5M).Count(); 16 | var queryToCsvFileResult = await dbContext.Database.SqlQueryToCsvFileAsync("SqlQueryToCsvFile-Test.csv", "SELECT * FROM Orders WHERE Price > @Price", new object[] { new SqlParameter("@Price", 5M) }); 17 | 18 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 19 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 20 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count + 1, "The total number of rows written to the file should match the count from the database plus the header row"); 21 | } 22 | [TestMethod] 23 | public async Task With_Options_ColumnDelimiter_TextQualifer() 24 | { 25 | var dbContext = SetupDbContext(true); 26 | string filePath = "SqlQueryToCsvFile_Options_ColumnDelimiter_TextQualifer-Test.csv"; 27 | int count = dbContext.Orders.Where(o => o.Price > 5M).Count(); 28 | var queryToCsvFileResult = await dbContext.Database.SqlQueryToCsvFileAsync(filePath, options => { options.ColumnDelimiter = "|"; options.TextQualifer = "\""; }, 29 | "SELECT * FROM Orders WHERE Price > @Price", new object[] { new SqlParameter("@Price", 5M) }); 30 | 31 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 32 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 33 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count + 1, "The total number of rows written to the file should match the count from the database plus the header row"); 34 | } 35 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DatabaseExtensions/SqlQuery_Count.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SqlClient; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace N.EntityFramework.Extensions.Test.DatabaseExtensions; 6 | 7 | [TestClass] 8 | public class SqlQuery_Count : DatabaseExtensionsBase 9 | { 10 | [TestMethod] 11 | public void With_Decimal_Value() 12 | { 13 | var dbContext = SetupDbContext(true); 14 | int efCount = dbContext.Orders.Where(o => o.Price > 5M).Count(); 15 | var sqlCount = dbContext.Database.FromSqlQuery("SELECT * FROM Orders WHERE Price > @Price", new SqlParameter("@Price", 5M)).Count(); 16 | 17 | Assert.IsTrue(efCount > 0, "Count from EF should be greater than zero"); 18 | Assert.IsTrue(efCount > 0, "Count from SQL should be greater than zero"); 19 | Assert.IsTrue(efCount == sqlCount, "Count from EF should match the count from the SqlQuery"); 20 | } 21 | [TestMethod] 22 | public void With_OrderBy() 23 | { 24 | var dbContext = SetupDbContext(true); 25 | int efCount = dbContext.Orders.Where(o => o.Price > 5M).Count(); 26 | var sqlCount = dbContext.Database.FromSqlQuery("SELECT * FROM Orders WHERE Price > @Price ORDER BY Id", new SqlParameter("@Price", 5M)).Count(); 27 | 28 | Assert.IsTrue(efCount > 0, "Count from EF should be greater than zero"); 29 | Assert.IsTrue(efCount > 0, "Count from SQL should be greater than zero"); 30 | Assert.IsTrue(efCount == sqlCount, "Count from EF should match the count from the SqlQuery"); 31 | } 32 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DatabaseExtensions/SqlQuery_CountAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Data.SqlClient; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DatabaseExtensions; 7 | 8 | [TestClass] 9 | public class SqlQuery_CountAsync : DatabaseExtensionsBase 10 | { 11 | [TestMethod] 12 | public async Task With_Decimal_Value() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | int efCount = dbContext.Orders.Where(o => o.Price > 5M).Count(); 16 | var sqlCount = await dbContext.Database.FromSqlQuery("SELECT * FROM Orders WHERE Price > @Price", new SqlParameter("@Price", 5M)).CountAsync(); 17 | 18 | Assert.IsTrue(efCount > 0, "Count from EF should be greater than zero"); 19 | Assert.IsTrue(efCount > 0, "Count from SQL should be greater than zero"); 20 | Assert.IsTrue(efCount == sqlCount, "Count from EF should match the count from the SqlQuery"); 21 | } 22 | [TestMethod] 23 | public async Task With_OrderBy() 24 | { 25 | var dbContext = SetupDbContext(true); 26 | int efCount = dbContext.Orders.Where(o => o.Price > 5M).Count(); 27 | var sqlCount = await dbContext.Database.FromSqlQuery("SELECT * FROM Orders WHERE Price > @Price ORDER BY Id", new SqlParameter("@Price", 5M)).CountAsync(); 28 | 29 | Assert.IsTrue(efCount > 0, "Count from EF should be greater than zero"); 30 | Assert.IsTrue(efCount > 0, "Count from SQL should be greater than zero"); 31 | Assert.IsTrue(efCount == sqlCount, "Count from EF should match the count from the SqlQuery"); 32 | } 33 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DatabaseExtensions/TableExists.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace N.EntityFramework.Extensions.Test.DatabaseExtensions; 5 | 6 | [TestClass] 7 | public class TableExists : DatabaseExtensionsBase 8 | { 9 | [TestMethod] 10 | public void With_Orders_Table() 11 | { 12 | var dbContext = SetupDbContext(true); 13 | int efCount = dbContext.Orders.Where(o => o.Price > 5M).Count(); 14 | bool ordersTableExists = dbContext.Database.TableExists("Orders"); 15 | bool orderNewTableExists = dbContext.Database.TableExists("OrdersNew"); 16 | 17 | Assert.IsTrue(ordersTableExists, "Orders table should exist"); 18 | Assert.IsTrue(!orderNewTableExists, "Orders_New table should not exist"); 19 | } 20 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DatabaseExtensions/TruncateTable.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace N.EntityFramework.Extensions.Test.DatabaseExtensions; 5 | 6 | [TestClass] 7 | public class TruncateTable : DatabaseExtensionsBase 8 | { 9 | [TestMethod] 10 | public void With_TableName() 11 | { 12 | var dbContext = SetupDbContext(true); 13 | int oldOrdersCount = dbContext.Orders.Count(); 14 | dbContext.Database.TruncateTable("Orders"); 15 | int newOrdersCount = dbContext.Orders.Count(); 16 | 17 | Assert.IsTrue(oldOrdersCount > 0, "Orders table should have data"); 18 | Assert.IsTrue(newOrdersCount == 0, "Order table should be empty after truncating"); 19 | } 20 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DatabaseExtensions/TruncateTableAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace N.EntityFramework.Extensions.Test.DatabaseExtensions; 6 | 7 | [TestClass] 8 | public class TruncateTableAsync : DatabaseExtensionsBase 9 | { 10 | [TestMethod] 11 | public async Task With_Orders_Table() 12 | { 13 | var dbContext = SetupDbContext(true); 14 | int oldOrdersCount = dbContext.Orders.Count(); 15 | await dbContext.Database.TruncateTableAsync("Orders"); 16 | int newOrdersCount = dbContext.Orders.Count(); 17 | 18 | Assert.IsTrue(oldOrdersCount > 0, "Orders table should have data"); 19 | Assert.IsTrue(newOrdersCount == 0, "Order table should be empty after truncating"); 20 | } 21 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/BulkDelete.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using N.EntityFramework.Extensions.Test.Data; 4 | 5 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 6 | 7 | [TestClass] 8 | public class BulkDelete : DbContextExtensionsBase 9 | { 10 | [TestMethod] 11 | public void With_Default_Options() 12 | { 13 | var dbContext = SetupDbContext(true); 14 | var orders = dbContext.Orders.Where(o => o.Price == 1.25M).ToList(); 15 | int rowsDeleted = dbContext.BulkDelete(orders); 16 | int newTotal = dbContext.Orders.Where(o => o.Price == 1.25M).Count(); 17 | 18 | Assert.IsTrue(orders.Count > 0, "There must be orders in database that match this condition (Price = $1.25)"); 19 | Assert.IsTrue(rowsDeleted == orders.Count, "The number of rows deleted must match the count of existing rows in database"); 20 | Assert.IsTrue(newTotal == 0, "Must be 0 to indicate all records were deleted"); 21 | } 22 | [TestMethod] 23 | public void With_Default_Options_Tpc() 24 | { 25 | var dbContext = SetupDbContext(true, PopulateDataMode.Tpc); 26 | var customers = dbContext.TpcPeople.OfType().ToList(); 27 | int rowsDeleted = dbContext.BulkDelete(customers, options => { options.DeleteOnCondition = (s, t) => s.Id == t.Id; }); 28 | var newCustomers = dbContext.TpcPeople.OfType().Count(); 29 | 30 | Assert.IsTrue(customers.Count > 0, "There must be tpcCustomer records in database"); 31 | Assert.IsTrue(rowsDeleted == customers.Count, "The number of rows deleted must match the count of existing rows in database"); 32 | Assert.IsTrue(newCustomers == 0, "Must be 0 to indicate all records were deleted"); 33 | } 34 | [TestMethod] 35 | public void With_Default_Options_Tph() 36 | { 37 | var dbContext = SetupDbContext(true, PopulateDataMode.Tph); 38 | var customers = dbContext.TphPeople.OfType().ToList(); 39 | int rowsDeleted = dbContext.BulkDelete(customers); 40 | var newCustomers = dbContext.TphPeople.OfType().Count(); 41 | 42 | Assert.IsTrue(customers.Count > 0, "There must be tphCustomer records in database"); 43 | Assert.IsTrue(rowsDeleted == customers.Count, "The number of rows deleted must match the count of existing rows in database"); 44 | Assert.IsTrue(newCustomers == 0, "Must be 0 to indicate all records were deleted"); 45 | } 46 | [TestMethod] 47 | public void With_Options_DeleteOnCondition() 48 | { 49 | var dbContext = SetupDbContext(true); 50 | int oldTotal = dbContext.Orders.Where(o => o.Price == 1.25M).Count(); 51 | var orders = dbContext.Orders.Where(o => o.Price == 1.25M && o.ExternalId != null).ToList(); 52 | int rowsDeleted = dbContext.BulkDelete(orders, options => { options.DeleteOnCondition = (s, t) => s.ExternalId == t.ExternalId; options.UsePermanentTable = true; }); 53 | int newTotal = dbContext.Orders.Where(o => o.Price == 1.25M).Count(); 54 | 55 | Assert.IsTrue(orders.Count > 0, "There must be orders in database that match this condition (Price < $2)"); 56 | Assert.IsTrue(rowsDeleted == orders.Count, "The number of rows deleted must match the count of existing rows in database"); 57 | Assert.IsTrue(newTotal == oldTotal - rowsDeleted, "Must be 0 to indicate all records were deleted"); 58 | } 59 | [TestMethod] 60 | public void With_Transaction() 61 | { 62 | var dbContext = SetupDbContext(true); 63 | var orders = dbContext.Orders.Where(o => o.Price == 1.25M).ToList(); 64 | int rowsDeleted, newTotal = 0; 65 | using (var transaction = dbContext.Database.BeginTransaction()) 66 | { 67 | rowsDeleted = dbContext.BulkDelete(orders); 68 | newTotal = dbContext.Orders.Where(o => o.Price == 1.25M).Count(); 69 | transaction.Rollback(); 70 | } 71 | var rollbackTotal = dbContext.Orders.Count(o => o.Price == 1.25M); 72 | 73 | Assert.IsTrue(orders.Count > 0, "There must be orders in database that match this condition (Price < $2)"); 74 | Assert.IsTrue(rowsDeleted == orders.Count, "The number of rows deleted must match the count of existing rows in database"); 75 | Assert.IsTrue(newTotal == 0, "Must be 0 to indicate all records were deleted"); 76 | Assert.IsTrue(rollbackTotal == orders.Count, "The number of rows after the transacation has been rollbacked should match the original count"); 77 | } 78 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/BulkDeleteAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using N.EntityFramework.Extensions.Test.Data; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 7 | 8 | [TestClass] 9 | public class BulkDeleteAsync : DbContextExtensionsBase 10 | { 11 | [TestMethod] 12 | public async Task With_Default_Options() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | var orders = dbContext.Orders.Where(o => o.Price == 1.25M).ToList(); 16 | int rowsDeleted = await dbContext.BulkDeleteAsync(orders); 17 | int newTotal = dbContext.Orders.Where(o => o.Price == 1.25M).Count(); 18 | 19 | Assert.IsTrue(orders.Count > 0, "There must be orders in database that match this condition (Price = $1.25)"); 20 | Assert.IsTrue(rowsDeleted == orders.Count, "The number of rows deleted must match the count of existing rows in database"); 21 | Assert.IsTrue(newTotal == 0, "Must be 0 to indicate all records were deleted"); 22 | } 23 | [TestMethod] 24 | public async Task With_Default_Options_Tpc() 25 | { 26 | var dbContext = SetupDbContext(true, PopulateDataMode.Tpc); 27 | var customers = dbContext.TpcPeople.OfType().ToList(); 28 | int rowsDeleted = await dbContext.BulkDeleteAsync(customers, options => { options.DeleteOnCondition = (s, t) => s.Id == t.Id; }); 29 | var newCustomers = dbContext.TpcPeople.OfType().Count(); 30 | 31 | Assert.IsTrue(customers.Count > 0, "There must be tpcCustomer records in database"); 32 | Assert.IsTrue(rowsDeleted == customers.Count, "The number of rows deleted must match the count of existing rows in database"); 33 | Assert.IsTrue(newCustomers == 0, "Must be 0 to indicate all records were deleted"); 34 | } 35 | [TestMethod] 36 | public async Task With_Default_Options_Tph() 37 | { 38 | var dbContext = SetupDbContext(true, PopulateDataMode.Tph); 39 | var customers = dbContext.TphPeople.OfType().ToList(); 40 | int rowsDeleted = await dbContext.BulkDeleteAsync(customers); 41 | var newCustomers = dbContext.TphPeople.OfType().Count(); 42 | 43 | Assert.IsTrue(customers.Count > 0, "There must be tphCustomer records in database"); 44 | Assert.IsTrue(rowsDeleted == customers.Count, "The number of rows deleted must match the count of existing rows in database"); 45 | Assert.IsTrue(newCustomers == 0, "Must be 0 to indicate all records were deleted"); 46 | } 47 | [TestMethod] 48 | public async Task With_Options_DeleteOnCondition() 49 | { 50 | var dbContext = SetupDbContext(true); 51 | int oldTotal = dbContext.Orders.Where(o => o.Price == 1.25M).Count(); 52 | var orders = dbContext.Orders.Where(o => o.Price == 1.25M && o.ExternalId != null).ToList(); 53 | int rowsDeleted = await dbContext.BulkDeleteAsync(orders, options => { options.DeleteOnCondition = (s, t) => s.ExternalId == t.ExternalId; options.UsePermanentTable = true; }); 54 | int newTotal = dbContext.Orders.Where(o => o.Price == 1.25M).Count(); 55 | 56 | Assert.IsTrue(orders.Count > 0, "There must be orders in database that match this condition (Price < $2)"); 57 | Assert.IsTrue(rowsDeleted == orders.Count, "The number of rows deleted must match the count of existing rows in database"); 58 | Assert.IsTrue(newTotal == oldTotal - rowsDeleted, "Must be 0 to indicate all records were deleted"); 59 | } 60 | [TestMethod] 61 | public async Task With_Transaction() 62 | { 63 | var dbContext = SetupDbContext(true); 64 | var orders = dbContext.Orders.Where(o => o.Price == 1.25M).ToList(); 65 | int rowsDeleted, newTotal = 0; 66 | using (var transaction = dbContext.Database.BeginTransaction()) 67 | { 68 | rowsDeleted = await dbContext.BulkDeleteAsync(orders); 69 | newTotal = dbContext.Orders.Where(o => o.Price == 1.25M).Count(); 70 | transaction.Rollback(); 71 | } 72 | var rollbackTotal = dbContext.Orders.Count(o => o.Price == 1.25M); 73 | 74 | Assert.IsTrue(orders.Count > 0, "There must be orders in database that match this condition (Price < $2)"); 75 | Assert.IsTrue(rowsDeleted == orders.Count, "The number of rows deleted must match the count of existing rows in database"); 76 | Assert.IsTrue(newTotal == 0, "Must be 0 to indicate all records were deleted"); 77 | Assert.IsTrue(rollbackTotal == orders.Count, "The number of rows after the transacation has been rollbacked should match the original count"); 78 | } 79 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/BulkFetch.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using N.EntityFramework.Extensions.Test.Data; 4 | 5 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 6 | 7 | [TestClass] 8 | public class BulkFetch : DbContextExtensionsBase 9 | { 10 | [TestMethod] 11 | public void With_Default_Options() 12 | { 13 | var dbContext = SetupDbContext(true); 14 | var orders = dbContext.Orders.Where(o => o.Price == 1.25M).ToList(); 15 | var fetchedOrders = dbContext.Orders.BulkFetch(orders); 16 | bool ordersAreMatched = true; 17 | 18 | foreach (var fetchedOrder in fetchedOrders) 19 | { 20 | var order = orders.First(o => o.Id == fetchedOrder.Id); 21 | if (order.ExternalId != fetchedOrder.ExternalId || order.AddedDateTime != fetchedOrder.AddedDateTime || order.ModifiedDateTime != fetchedOrder.ModifiedDateTime) 22 | { 23 | ordersAreMatched = false; 24 | break; 25 | } 26 | } 27 | 28 | Assert.IsTrue(orders.Count > 0, "There must be orders in database that match this condition (Price = $1.25)"); 29 | Assert.IsTrue(orders.Count == fetchedOrders.Count(), "The number of rows deleted must match the count of existing rows in database"); 30 | Assert.IsTrue(ordersAreMatched, "The orders from BulkFetch() should match what is retrieved from DbContext"); 31 | } 32 | [TestMethod] 33 | public void With_IQueryable() 34 | { 35 | var dbContext = SetupDbContext(true); 36 | var orders = dbContext.Orders.Where(o => o.Price <= 10 && o.ExternalId != null); 37 | var fetchedOrders = dbContext.Orders.BulkFetch(orders, options => { options.IgnoreColumns = o => new { o.ExternalId }; }).ToList(); 38 | int newTotal = dbContext.Orders.Where(o => o.Price <= 10 && o.ExternalId == null).Count(); 39 | bool foundNullExternalId = fetchedOrders.Where(o => o.ExternalId != null).Any(); 40 | 41 | Assert.IsTrue(orders.Count() > 0, "There must be orders in the database that match condition (Price <= 10 And ExternalId != null)"); 42 | Assert.IsTrue(orders.Count() == fetchedOrders.Count(), "The number of orders must match the number of fetched orders"); 43 | Assert.IsTrue(!foundNullExternalId, "Fetched orders should not contain any items where ExternalId is null."); 44 | } 45 | [TestMethod] 46 | public void With_Options_IgnoreColumns() 47 | { 48 | var dbContext = SetupDbContext(true); 49 | var orders = dbContext.Orders.Where(o => o.Price <= 10 && o.ExternalId != null).ToList(); 50 | var fetchedOrders = dbContext.Orders.BulkFetch(orders, options => { options.IgnoreColumns = o => new { o.ExternalId }; }).ToList(); 51 | int newTotal = dbContext.Orders.Where(o => o.Price <= 10 && o.ExternalId == null).Count(); 52 | bool foundNullExternalId = fetchedOrders.Where(o => o.ExternalId != null).Any(); 53 | 54 | Assert.IsTrue(orders.Count() > 0, "There must be orders in the database that match condition (Price <= 10 And ExternalId != null)"); 55 | Assert.IsTrue(orders.Count() == fetchedOrders.Count(), "The number of orders must match the number of fetched orders"); 56 | Assert.IsTrue(!foundNullExternalId, "Fetched orders should not contain any items where ExternalId is null."); 57 | } 58 | //[TestMethod] 59 | //public void With_Options_InputColumns() 60 | //{ 61 | // var dbContext = SetupDbContext(false); 62 | // var orders = new List(); 63 | // for (int i = 0; i < 20000; i++) 64 | // { 65 | // orders.Add(new Order { Id = i, ExternalId = i.ToString(), Price = 1.57M, Active = true }); 66 | // } 67 | // int oldTotal = dbContext.Orders.Where(o => o.Price == 1.57M && o.ExternalId == null && o.Active == true).Count(); 68 | // int rowsInserted = dbContext.BulkInsert(orders, options => { options.UsePermanentTable = true; options.InputColumns = o => new { o.Price, o.Active, o.AddedDateTime }; }); 69 | // int newTotal = dbContext.Orders.Where(o => o.Price == 1.57M && o.ExternalId == null && o.Active == true).Count(); 70 | 71 | // Assert.IsTrue(rowsInserted == orders.Count, "The number of rows inserted must match the count of order list"); 72 | // Assert.IsTrue(newTotal - oldTotal == rowsInserted, "The new count minus the old count should match the number of rows inserted."); 73 | //} 74 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/BulkSync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using N.EntityFramework.Extensions.Test.Data; 6 | 7 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 8 | 9 | [TestClass] 10 | public class BulkSync : DbContextExtensionsBase 11 | { 12 | [TestMethod] 13 | public void With_Default_Options() 14 | { 15 | var dbContext = SetupDbContext(true); 16 | int oldTotal = dbContext.Orders.Count(); 17 | var orders = dbContext.Orders.Where(o => o.Id <= 10000).OrderBy(o => o.Id).ToList(); 18 | int ordersToAdd = 5000; 19 | int ordersToUpdate = orders.Count; 20 | foreach (var order in orders) 21 | { 22 | order.Price = Convert.ToDecimal(order.Id + .25); 23 | } 24 | for (int i = 0; i < ordersToAdd; i++) 25 | { 26 | orders.Add(new Order { Id = 100000 + i, Price = 3.55M }); 27 | } 28 | var result = dbContext.BulkSync(orders); 29 | var newOrders = dbContext.Orders.OrderBy(o => o.Id).ToList(); 30 | bool areAddedOrdersMerged = true; 31 | bool areUpdatedOrdersMerged = true; 32 | foreach (var newOrder in newOrders.Where(o => o.Id <= 10000).OrderBy(o => o.Id)) 33 | { 34 | if (newOrder.Price != Convert.ToDecimal(newOrder.Id + .25)) 35 | { 36 | areUpdatedOrdersMerged = false; 37 | break; 38 | } 39 | } 40 | foreach (var newOrder in newOrders.Where(o => o.Id >= 500000).OrderBy(o => o.Id)) 41 | { 42 | if (newOrder.Price != 3.55M) 43 | { 44 | areAddedOrdersMerged = false; 45 | break; 46 | } 47 | } 48 | 49 | Assert.IsTrue(result.RowsAffected == oldTotal + ordersToAdd, "The number of rows inserted must match the count of order list"); 50 | Assert.IsTrue(result.RowsUpdated == ordersToUpdate, "The number of rows updated must match"); 51 | Assert.IsTrue(result.RowsInserted == ordersToAdd, "The number of rows added must match"); 52 | Assert.IsTrue(result.RowsDeleted == oldTotal - orders.Count() + ordersToAdd, "The number of rows deleted must match the difference from the total existing orders to the new orders to add/update"); 53 | Assert.IsTrue(areAddedOrdersMerged, "The orders that were added did not merge correctly"); 54 | Assert.IsTrue(areUpdatedOrdersMerged, "The orders that were updated did not merge correctly"); 55 | } 56 | [TestMethod] 57 | public void With_Options_AutoMapIdentity() 58 | { 59 | var dbContext = SetupDbContext(true); 60 | int oldTotal = dbContext.Orders.Count(); 61 | int ordersToUpdate = 3; 62 | int ordersToAdd = 2; 63 | var orders = new List 64 | { 65 | new Order { ExternalId = "id-1", Price=7.10M }, 66 | new Order { ExternalId = "id-2", Price=9.33M }, 67 | new Order { ExternalId = "id-3", Price=3.25M }, 68 | new Order { ExternalId = "id-1000001", Price=2.15M }, 69 | new Order { ExternalId = "id-1000002", Price=5.75M }, 70 | }; 71 | var result = dbContext.BulkSync(orders, options => { options.MergeOnCondition = (s, t) => s.ExternalId == t.ExternalId; options.UsePermanentTable = true; }); 72 | bool autoMapIdentityMatched = true; 73 | foreach (var order in orders) 74 | { 75 | if (!dbContext.Orders.Any(o => o.ExternalId == order.ExternalId && o.Id == order.Id && o.Price == order.Price)) 76 | { 77 | autoMapIdentityMatched = false; 78 | break; 79 | } 80 | } 81 | 82 | Assert.IsTrue(result.RowsAffected == oldTotal + ordersToAdd, "The number of rows inserted must match the count of order list"); 83 | Assert.IsTrue(result.RowsUpdated == ordersToUpdate, "The number of rows updated must match"); 84 | Assert.IsTrue(result.RowsInserted == ordersToAdd, "The number of rows added must match"); 85 | Assert.IsTrue(result.RowsDeleted == oldTotal - orders.Count() + ordersToAdd, "The number of rows deleted must match the difference from the total existing orders to the new orders to add/update"); 86 | Assert.IsTrue(autoMapIdentityMatched, "The auto mapping of ids of entities that were merged failed to match up"); 87 | } 88 | [TestMethod] 89 | public void With_Options_MergeOnCondition() 90 | { 91 | var dbContext = SetupDbContext(true); 92 | int oldTotal = dbContext.Orders.Count(); 93 | var orders = dbContext.Orders.Where(o => o.Id <= 100 && o.ExternalId != null).OrderBy(o => o.Id).ToList(); 94 | int ordersToAdd = 50; 95 | int ordersToUpdate = orders.Count; 96 | foreach (var order in orders) 97 | { 98 | order.Price = Convert.ToDecimal(order.Id + .25); 99 | } 100 | for (int i = 0; i < ordersToAdd; i++) 101 | { 102 | orders.Add(new Order { Id = 100000 + i, Price = 3.55M }); 103 | } 104 | var result = dbContext.BulkSync(orders, new BulkSyncOptions 105 | { 106 | MergeOnCondition = (s, t) => s.ExternalId == t.ExternalId, 107 | BatchSize = 1000 108 | }); 109 | var newOrders = dbContext.Orders.OrderBy(o => o.Id).ToList(); 110 | bool areAddedOrdersMerged = true; 111 | bool areUpdatedOrdersMerged = true; 112 | foreach (var newOrder in newOrders.Where(o => o.Id <= 100 && o.ExternalId != null).OrderBy(o => o.Id)) 113 | { 114 | if (newOrder.Price != Convert.ToDecimal(newOrder.Id + .25)) 115 | { 116 | areUpdatedOrdersMerged = false; 117 | break; 118 | } 119 | } 120 | foreach (var newOrder in newOrders.Where(o => o.Id >= 500000).OrderBy(o => o.Id)) 121 | { 122 | if (newOrder.Price != 3.55M) 123 | { 124 | areAddedOrdersMerged = false; 125 | break; 126 | } 127 | } 128 | 129 | Assert.IsTrue(result.RowsAffected == oldTotal + ordersToAdd, "The number of rows inserted must match the count of order list"); 130 | Assert.IsTrue(result.RowsUpdated == ordersToUpdate, "The number of rows updated must match"); 131 | Assert.IsTrue(result.RowsInserted == ordersToAdd, "The number of rows added must match"); 132 | Assert.IsTrue(result.RowsDeleted == oldTotal - orders.Count() + ordersToAdd, "The number of rows deleted must match the difference from the total existing orders to the new orders to add/update"); 133 | Assert.IsTrue(areAddedOrdersMerged, "The orders that were added did not merge correctly"); 134 | Assert.IsTrue(areUpdatedOrdersMerged, "The orders that were updated did not merge correctly"); 135 | } 136 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/BulkSyncAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using N.EntityFramework.Extensions.Test.Data; 7 | 8 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 9 | 10 | [TestClass] 11 | public class BulkSyncAsync : DbContextExtensionsBase 12 | { 13 | [TestMethod] 14 | public async Task With_Default_Options() 15 | { 16 | var dbContext = SetupDbContext(true); 17 | int oldTotal = dbContext.Orders.Count(); 18 | var orders = dbContext.Orders.Where(o => o.Id <= 10000).OrderBy(o => o.Id).ToList(); 19 | int ordersToAdd = 5000; 20 | int ordersToUpdate = orders.Count; 21 | foreach (var order in orders) 22 | { 23 | order.Price = Convert.ToDecimal(order.Id + .25); 24 | } 25 | for (int i = 0; i < ordersToAdd; i++) 26 | { 27 | orders.Add(new Order { Id = 100000 + i, Price = 3.55M }); 28 | } 29 | var result = await dbContext.BulkSyncAsync(orders); 30 | var newOrders = dbContext.Orders.OrderBy(o => o.Id).ToList(); 31 | bool areAddedOrdersMerged = true; 32 | bool areUpdatedOrdersMerged = true; 33 | foreach (var newOrder in newOrders.Where(o => o.Id <= 10000).OrderBy(o => o.Id)) 34 | { 35 | if (newOrder.Price != Convert.ToDecimal(newOrder.Id + .25)) 36 | { 37 | areUpdatedOrdersMerged = false; 38 | break; 39 | } 40 | } 41 | foreach (var newOrder in newOrders.Where(o => o.Id >= 500000).OrderBy(o => o.Id)) 42 | { 43 | if (newOrder.Price != 3.55M) 44 | { 45 | areAddedOrdersMerged = false; 46 | break; 47 | } 48 | } 49 | 50 | Assert.IsTrue(result.RowsAffected == oldTotal + ordersToAdd, "The number of rows inserted must match the count of order list"); 51 | Assert.IsTrue(result.RowsUpdated == ordersToUpdate, "The number of rows updated must match"); 52 | Assert.IsTrue(result.RowsInserted == ordersToAdd, "The number of rows added must match"); 53 | Assert.IsTrue(result.RowsDeleted == oldTotal - orders.Count() + ordersToAdd, "The number of rows deleted must match the difference from the total existing orders to the new orders to add/update"); 54 | Assert.IsTrue(areAddedOrdersMerged, "The orders that were added did not merge correctly"); 55 | Assert.IsTrue(areUpdatedOrdersMerged, "The orders that were updated did not merge correctly"); 56 | } 57 | [TestMethod] 58 | public async Task With_Options_AutoMapIdentity() 59 | { 60 | var dbContext = SetupDbContext(true); 61 | int oldTotal = dbContext.Orders.Count(); 62 | int ordersToUpdate = 3; 63 | int ordersToAdd = 2; 64 | var orders = new List 65 | { 66 | new Order { ExternalId = "id-1", Price=7.10M }, 67 | new Order { ExternalId = "id-2", Price=9.33M }, 68 | new Order { ExternalId = "id-3", Price=3.25M }, 69 | new Order { ExternalId = "id-1000001", Price=2.15M }, 70 | new Order { ExternalId = "id-1000002", Price=5.75M }, 71 | }; 72 | var result = await dbContext.BulkSyncAsync(orders, options => { options.MergeOnCondition = (s, t) => s.ExternalId == t.ExternalId; options.UsePermanentTable = true; }); 73 | bool autoMapIdentityMatched = true; 74 | foreach (var order in orders) 75 | { 76 | if (!dbContext.Orders.Any(o => o.ExternalId == order.ExternalId && o.Id == order.Id && o.Price == order.Price)) 77 | { 78 | autoMapIdentityMatched = false; 79 | break; 80 | } 81 | } 82 | 83 | Assert.IsTrue(result.RowsAffected == oldTotal + ordersToAdd, "The number of rows inserted must match the count of order list"); 84 | Assert.IsTrue(result.RowsUpdated == ordersToUpdate, "The number of rows updated must match"); 85 | Assert.IsTrue(result.RowsInserted == ordersToAdd, "The number of rows added must match"); 86 | Assert.IsTrue(result.RowsDeleted == oldTotal - orders.Count() + ordersToAdd, "The number of rows deleted must match the difference from the total existing orders to the new orders to add/update"); 87 | Assert.IsTrue(autoMapIdentityMatched, "The auto mapping of ids of entities that were merged failed to match up"); 88 | } 89 | [TestMethod] 90 | public async Task With_Options_MergeOnCondition() 91 | { 92 | var dbContext = SetupDbContext(true); 93 | int oldTotal = dbContext.Orders.Count(); 94 | var orders = dbContext.Orders.Where(o => o.Id <= 100 && o.ExternalId != null).OrderBy(o => o.Id).ToList(); 95 | int ordersToAdd = 50; 96 | int ordersToUpdate = orders.Count; 97 | foreach (var order in orders) 98 | { 99 | order.Price = Convert.ToDecimal(order.Id + .25); 100 | } 101 | for (int i = 0; i < ordersToAdd; i++) 102 | { 103 | orders.Add(new Order { Id = 100000 + i, Price = 3.55M }); 104 | } 105 | var result = await dbContext.BulkSyncAsync(orders, new BulkSyncOptions 106 | { 107 | MergeOnCondition = (s, t) => s.ExternalId == t.ExternalId, 108 | BatchSize = 1000 109 | }); 110 | var newOrders = dbContext.Orders.OrderBy(o => o.Id).ToList(); 111 | bool areAddedOrdersMerged = true; 112 | bool areUpdatedOrdersMerged = true; 113 | foreach (var newOrder in newOrders.Where(o => o.Id <= 100 && o.ExternalId != null).OrderBy(o => o.Id)) 114 | { 115 | if (newOrder.Price != Convert.ToDecimal(newOrder.Id + .25)) 116 | { 117 | areUpdatedOrdersMerged = false; 118 | break; 119 | } 120 | } 121 | foreach (var newOrder in newOrders.Where(o => o.Id >= 500000).OrderBy(o => o.Id)) 122 | { 123 | if (newOrder.Price != 3.55M) 124 | { 125 | areAddedOrdersMerged = false; 126 | break; 127 | } 128 | } 129 | 130 | Assert.IsTrue(result.RowsAffected == oldTotal + ordersToAdd, "The number of rows inserted must match the count of order list"); 131 | Assert.IsTrue(result.RowsUpdated == ordersToUpdate, "The number of rows updated must match"); 132 | Assert.IsTrue(result.RowsInserted == ordersToAdd, "The number of rows added must match"); 133 | Assert.IsTrue(result.RowsDeleted == oldTotal - orders.Count() + ordersToAdd, "The number of rows deleted must match the difference from the total existing orders to the new orders to add/update"); 134 | Assert.IsTrue(areAddedOrdersMerged, "The orders that were added did not merge correctly"); 135 | Assert.IsTrue(areUpdatedOrdersMerged, "The orders that were updated did not merge correctly"); 136 | } 137 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/DbContextExtensionsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using N.EntityFramework.Extensions.Test.Data; 6 | 7 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 8 | 9 | public enum PopulateDataMode 10 | { 11 | Normal, 12 | Tpc, 13 | Tph, 14 | Schema 15 | } 16 | [TestClass] 17 | public class DbContextExtensionsBase 18 | { 19 | [TestInitialize] 20 | public void Init() 21 | { 22 | var dbContext = new TestDbContext(); 23 | dbContext.Database.CreateIfNotExists(); 24 | } 25 | protected static TestDbContext SetupDbContext(bool populateData, PopulateDataMode mode = PopulateDataMode.Normal) 26 | { 27 | var dbContext = new TestDbContext(); 28 | dbContext.Orders.Truncate(); 29 | dbContext.Products.Truncate(); 30 | dbContext.ProductCategories.Clear(); 31 | dbContext.ProductsWithCustomSchema.Truncate(); 32 | dbContext.ProductsWithComplexKey.Truncate(); 33 | dbContext.ProductsWithTrigger.Truncate(); 34 | dbContext.Database.ClearTable("TphPeople"); 35 | dbContext.Database.ClearTable("TpcCustomer"); 36 | dbContext.Database.ClearTable("TpcVendor"); 37 | dbContext.Database.DropTable("OrdersUnderTen", true); 38 | dbContext.Database.DropTable("OrdersLast30Days", true); 39 | dbContext.Database.DropTable("top.ProductsUnderTen", true); 40 | if (populateData) 41 | { 42 | if (mode == PopulateDataMode.Normal) 43 | { 44 | var orders = new List(); 45 | int id = 1; 46 | for (int i = 0; i < 2050; i++) 47 | { 48 | DateTime addedDateTime = DateTime.UtcNow.AddDays(-id); 49 | orders.Add(new Order 50 | { 51 | Id = id, 52 | ExternalId = string.Format("id-{0}", i), 53 | Price = 1.25M, 54 | AddedDateTime = addedDateTime, 55 | ModifiedDateTime = addedDateTime.AddHours(3) 56 | }); 57 | id++; 58 | } 59 | for (int i = 0; i < 1050; i++) 60 | { 61 | orders.Add(new Order { Id = id, Price = 5.35M }); 62 | id++; 63 | } 64 | for (int i = 0; i < 2050; i++) 65 | { 66 | orders.Add(new Order { Id = id, Price = 1.25M }); 67 | id++; 68 | } 69 | for (int i = 0; i < 6000; i++) 70 | { 71 | orders.Add(new Order { Id = id, Price = 15.35M }); 72 | id++; 73 | } 74 | for (int i = 0; i < 6000; i++) 75 | { 76 | orders.Add(new Order { Id = id, Price = 15.35M }); 77 | id++; 78 | } 79 | 80 | Debug.WriteLine("Last Id for Order is {0}", id); 81 | dbContext.BulkInsert(orders, new BulkInsertOptions() { KeepIdentity = true }); 82 | 83 | var productCategories = new List() 84 | { 85 | new ProductCategory { Id=1, Name="Category-1", Active=true}, 86 | new ProductCategory { Id=2, Name="Category-2", Active=true}, 87 | new ProductCategory { Id=3, Name="Category-3", Active=true}, 88 | new ProductCategory { Id=4, Name="Category-4", Active=false}, 89 | }; 90 | dbContext.BulkInsert(productCategories, o => { o.KeepIdentity = true; o.UsePermanentTable = true; }); 91 | var products = new List(); 92 | id = 1; 93 | for (int i = 0; i < 2050; i++) 94 | { 95 | products.Add(new Product { Id = i.ToString(), Price = 1.25M, OutOfStock = false, ProductCategoryId = 4 }); 96 | id++; 97 | } 98 | for (int i = 2050; i < 7000; i++) 99 | { 100 | products.Add(new Product { Id = i.ToString(), Price = 5.75M, OutOfStock = true }); 101 | id++; 102 | } 103 | 104 | Debug.WriteLine("Last Id for Product is {0}", id); 105 | dbContext.BulkInsert(products, new BulkInsertOptions() { KeepIdentity = false, AutoMapOutput = false }); 106 | 107 | //ProductWithComplexKey 108 | var productsWithComplexKey = new List(); 109 | id = 1; 110 | 111 | for (int i = 0; i < 2050; i++) 112 | { 113 | productsWithComplexKey.Add(new ProductWithComplexKey { Price = 1.25M }); 114 | id++; 115 | } 116 | 117 | Debug.WriteLine("Last Id for ProductsWithComplexKey is {0}", id); 118 | dbContext.BulkInsert(productsWithComplexKey, new BulkInsertOptions() { KeepIdentity = false, AutoMapOutput = false }); 119 | } 120 | else if (mode == PopulateDataMode.Tph) 121 | { 122 | //TPH Customers & Vendors 123 | var tphCustomers = new List(); 124 | var tphVendors = new List(); 125 | for (int i = 0; i < 2000; i++) 126 | { 127 | tphCustomers.Add(new TphCustomer 128 | { 129 | Id = i, 130 | FirstName = string.Format("John_{0}", i), 131 | LastName = string.Format("Smith_{0}", i), 132 | Email = string.Format("john.smith{0}@domain.com", i), 133 | Phone = "404-555-1111", 134 | AddedDate = DateTime.UtcNow 135 | }); 136 | } 137 | for (int i = 2000; i < 3000; i++) 138 | { 139 | tphVendors.Add(new TphVendor 140 | { 141 | Id = i, 142 | FirstName = string.Format("Mike_{0}", i), 143 | LastName = string.Format("Smith_{0}", i), 144 | Phone = "404-555-2222", 145 | Email = string.Format("mike.smith{0}@domain.com", i), 146 | Url = string.Format("http://domain.com/mike.smith{0}", i) 147 | }); 148 | } 149 | dbContext.BulkInsert(tphCustomers, new BulkInsertOptions() { KeepIdentity = true }); 150 | dbContext.BulkInsert(tphVendors, new BulkInsertOptions() { KeepIdentity = true }); 151 | } 152 | else if (mode == PopulateDataMode.Tpc) 153 | { 154 | //TPC Customers & Vendors 155 | var tpcCustomers = new List(); 156 | var tpcVendors = new List(); 157 | for (int i = 0; i < 2000; i++) 158 | { 159 | tpcCustomers.Add(new TpcCustomer 160 | { 161 | Id = i, 162 | FirstName = string.Format("John_{0}", i), 163 | LastName = string.Format("Smith_{0}", i), 164 | Email = string.Format("john.smith{0}@domain.com", i), 165 | Phone = "404-555-1111", 166 | AddedDate = DateTime.UtcNow 167 | }); 168 | } 169 | for (int i = 2000; i < 3000; i++) 170 | { 171 | tpcVendors.Add(new TpcVendor 172 | { 173 | Id = i, 174 | FirstName = string.Format("Mike_{0}", i), 175 | LastName = string.Format("Smith_{0}", i), 176 | Phone = "404-555-2222", 177 | Email = string.Format("mike.smith{0}@domain.com", i), 178 | Url = string.Format("http://domain.com/mike.smith{0}", i) 179 | }); 180 | } 181 | dbContext.BulkInsert(tpcCustomers, new BulkInsertOptions() { KeepIdentity = true }); 182 | dbContext.BulkInsert(tpcVendors, new BulkInsertOptions() { KeepIdentity = true }); 183 | } 184 | else if (mode == PopulateDataMode.Schema) 185 | { 186 | //ProductWithCustomSchema 187 | var productsWithCustomSchema = new List(); 188 | int id = 1; 189 | 190 | for (int i = 0; i < 2050; i++) 191 | { 192 | productsWithCustomSchema.Add(new ProductWithCustomSchema { Id = id.ToString(), Price = 1.25M }); 193 | id++; 194 | } 195 | for (int i = 2050; i < 5000; i++) 196 | { 197 | productsWithCustomSchema.Add(new ProductWithCustomSchema { Id = id.ToString(), Price = 6.75M }); 198 | id++; 199 | } 200 | 201 | dbContext.BulkInsert(productsWithCustomSchema); 202 | } 203 | } 204 | return dbContext; 205 | } 206 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/DeleteFromQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using N.EntityFramework.Extensions.Test.Data; 7 | 8 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 9 | 10 | [TestClass] 11 | public class DeleteFromQuery : DbContextExtensionsBase 12 | { 13 | [TestMethod] 14 | public void With_Boolean_Value() 15 | { 16 | var dbContext = SetupDbContext(true); 17 | var products = dbContext.Products.Where(p => p.OutOfStock); 18 | int oldTotal = products.Count(a => a.OutOfStock); 19 | int rowUpdated = products.DeleteFromQuery(); 20 | int newTotal = dbContext.Products.Count(o => o.OutOfStock); 21 | 22 | Assert.IsTrue(oldTotal > 0, "There must be products in database that match this condition (OutOfStock == true)"); 23 | Assert.IsTrue(rowUpdated == oldTotal, "The number of rows update must match the count of rows that match the condition (OutOfStock == false)"); 24 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were updated"); 25 | } 26 | [TestMethod] 27 | public void With_Child_Relationship() 28 | { 29 | var dbContext = SetupDbContext(true); 30 | var products = dbContext.Products.Where(p => !p.ProductCategory.Active); 31 | int oldTotal = products.Count(); 32 | int rowsDeleted = products.DeleteFromQuery(); 33 | int newTotal = products.Count(); 34 | 35 | Assert.IsTrue(oldTotal > 0, "There must be products in database that match this condition (ProductCategory.Active == false)"); 36 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows update must match the count of rows that match the condition (ProductCategory.Active == false)"); 37 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 38 | } 39 | [TestMethod] 40 | public void With_Contains_Empty_List() 41 | { 42 | var dbContext = SetupDbContext(false); 43 | var emptyList = new List(); 44 | var orders = dbContext.Orders.Where(o => emptyList.Contains(o.Id)); 45 | int oldTotal = orders.Count(); 46 | int rowsDeleted = orders.DeleteFromQuery(); 47 | int newTotal = orders.Count(); 48 | 49 | Assert.IsTrue(oldTotal == 0, "There must be no orders in database that match this condition"); 50 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 51 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 52 | } 53 | [TestMethod] 54 | public void With_Contains_Large_List() 55 | { 56 | var dbContext = SetupDbContext(true); 57 | var ids = new long[10000]; 58 | for (int i = 0; i < ids.Length; i++) 59 | { 60 | ids[i] = i + 1; 61 | } 62 | int rowsDeleted = dbContext.Orders.Where(a => ids.Contains(a.Id)).DeleteFromQuery(); 63 | 64 | Assert.IsTrue(rowsDeleted == ids.Length, "There number of rows deleted should match the length of the Ids array"); 65 | } 66 | [TestMethod] 67 | public void With_Contains_Integer_List() 68 | { 69 | var dbContext = SetupDbContext(true); 70 | var emptyList = new List() { 1, 2, 3, 4, 5 }; 71 | var orders = dbContext.Orders.Where(o => emptyList.Contains(o.Id)); 72 | int oldTotal = orders.Count(); 73 | int rowsDeleted = orders.DeleteFromQuery(); 74 | int newTotal = orders.Count(); 75 | 76 | Assert.IsTrue(oldTotal > 0, "There must be orders in database to delete"); 77 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 78 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 79 | } 80 | [TestMethod] 81 | public void With_Decimal_Using_IQuerable() 82 | { 83 | var dbContext = SetupDbContext(true); 84 | var orders = dbContext.Orders.Where(o => o.Price <= 10); 85 | int oldTotal = orders.Count(); 86 | int rowsDeleted = orders.DeleteFromQuery(); 87 | int newTotal = orders.Count(); 88 | 89 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 90 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 91 | Assert.IsTrue(newTotal == 0, "Delete() Failed: must be 0 to indicate all records were deleted"); 92 | } 93 | [TestMethod] 94 | public void With_Decimal_Using_IEnumerable() 95 | { 96 | var dbContext = SetupDbContext(true); 97 | var orders = dbContext.Orders.Where(o => o.Price <= 10); 98 | int oldTotal = orders.Count(); 99 | int rowsDeleted = orders.DeleteFromQuery(); 100 | int newTotal = orders.Count(); 101 | 102 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 103 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 104 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 105 | } 106 | [TestMethod] 107 | public void With_DateTime() 108 | { 109 | var dbContext = SetupDbContext(true); 110 | int oldTotal = dbContext.Orders.Count(); 111 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 112 | int rowsToDelete = dbContext.Orders.Where(o => o.ModifiedDateTime != null && o.ModifiedDateTime >= dateTime).Count(); 113 | int rowsDeleted = dbContext.Orders.Where(o => o.ModifiedDateTime != null && o.ModifiedDateTime >= dateTime) 114 | .DeleteFromQuery(); 115 | int newTotal = dbContext.Orders.Count(); 116 | 117 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 118 | Assert.IsTrue(rowsDeleted == rowsToDelete, "The number of rows deleted must match the count of the rows that matched in the database"); 119 | Assert.IsTrue(oldTotal - newTotal == rowsDeleted, "The rows deleted must match the new count minues the old count"); 120 | } 121 | [TestMethod] 122 | public void With_Delete_All() 123 | { 124 | var dbContext = SetupDbContext(true); 125 | int oldTotal = dbContext.Orders.Count(); 126 | int rowsDeleted = dbContext.Orders.DeleteFromQuery(); 127 | int newTotal = dbContext.Orders.Count(); 128 | 129 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 130 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 131 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 132 | } 133 | [TestMethod] 134 | public void With_Different_Values() 135 | { 136 | var dbContext = SetupDbContext(true); 137 | int oldTotal = dbContext.Orders.Count(); 138 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 139 | var orders = dbContext.Orders.Where(o => o.Id == 1 && o.Active && o.ModifiedDateTime >= dateTime); 140 | int rowsToDelete = orders.Count(); 141 | int rowsDeleted = orders.DeleteFromQuery(); 142 | int newTotal = dbContext.Orders.Count(); 143 | 144 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 145 | Assert.IsTrue(rowsDeleted == rowsToDelete, "The number of rows deleted must match the count of the rows that matched in the database"); 146 | Assert.IsTrue(oldTotal - newTotal == rowsDeleted, "The rows deleted must match the new count minues the old count"); 147 | } 148 | [TestMethod] 149 | public void With_Schema() 150 | { 151 | var dbContext = SetupDbContext(true, PopulateDataMode.Schema); 152 | int oldTotal = dbContext.ProductsWithCustomSchema.Count(); 153 | int rowsDeleted = dbContext.ProductsWithCustomSchema.DeleteFromQuery(); 154 | int newTotal = dbContext.ProductsWithCustomSchema.Count(); 155 | 156 | Assert.IsTrue(oldTotal > 0, "There must be products in database that match this condition"); 157 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 158 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 159 | } 160 | [TestMethod] 161 | public void With_Transaction() 162 | { 163 | var dbContext = SetupDbContext(true); 164 | int rowsDeleted; 165 | int oldTotal = dbContext.Orders.Count(); 166 | var orders = dbContext.Orders.Where(o => o.Price <= 10); 167 | int rowsToDelete = orders.Count(); 168 | using (var transaction = dbContext.Database.BeginTransaction()) 169 | { 170 | rowsDeleted = orders.DeleteFromQuery(); 171 | transaction.Rollback(); 172 | } 173 | int newTotal = dbContext.Orders.Count(); 174 | 175 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition (Price < $10)"); 176 | Assert.IsTrue(rowsDeleted == orders.Count(), "The number of rows update must match the count of rows that match the condtion (Price < $10)"); 177 | Assert.IsTrue(newTotal == oldTotal, "The new count must match the old count since the transaction was rollbacked"); 178 | } 179 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/DeleteFromQueryAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using N.EntityFramework.Extensions.Test.Data; 7 | 8 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 9 | 10 | [TestClass] 11 | public class DeleteFromQueryAsync : DbContextExtensionsBase 12 | { 13 | [TestMethod] 14 | public async Task With_Boolean_Value() 15 | { 16 | var dbContext = SetupDbContext(true); 17 | var products = dbContext.Products.Where(p => p.OutOfStock); 18 | int oldTotal = products.Count(a => a.OutOfStock); 19 | int rowUpdated = await products.DeleteFromQueryAsync(); 20 | int newTotal = dbContext.Products.Count(o => o.OutOfStock); 21 | 22 | Assert.IsTrue(oldTotal > 0, "There must be products in database that match this condition (OutOfStock == true)"); 23 | Assert.IsTrue(rowUpdated == oldTotal, "The number of rows update must match the count of rows that match the condition (OutOfStock == false)"); 24 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were updated"); 25 | } 26 | [TestMethod] 27 | public async Task With_Child_Relationship() 28 | { 29 | var dbContext = SetupDbContext(true); 30 | var products = dbContext.Products.Where(p => !p.ProductCategory.Active); 31 | int oldTotal = products.Count(); 32 | int rowsDeleted = await products.DeleteFromQueryAsync(); 33 | int newTotal = products.Count(); 34 | 35 | Assert.IsTrue(oldTotal > 0, "There must be products in database that match this condition (ProductCategory.Active == false)"); 36 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows update must match the count of rows that match the condition (ProductCategory.Active == false)"); 37 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 38 | } 39 | [TestMethod] 40 | public async Task With_Contains_Empty_List() 41 | { 42 | var dbContext = SetupDbContext(false); 43 | var emptyList = new List(); 44 | var orders = dbContext.Orders.Where(o => emptyList.Contains(o.Id)); 45 | int oldTotal = orders.Count(); 46 | int rowsDeleted = await orders.DeleteFromQueryAsync(); 47 | int newTotal = orders.Count(); 48 | 49 | Assert.IsTrue(oldTotal == 0, "There must be no orders in database that match this condition"); 50 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 51 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 52 | } 53 | [TestMethod] 54 | public async Task With_Contains_Large_List() 55 | { 56 | var dbContext = SetupDbContext(true); 57 | var ids = new long[10000]; 58 | for (int i = 0; i < ids.Length; i++) 59 | { 60 | ids[i] = i + 1; 61 | } 62 | int rowsDeleted = await dbContext.Orders.Where(a => ids.Contains(a.Id)).DeleteFromQueryAsync(); 63 | 64 | Assert.IsTrue(rowsDeleted == ids.Length, "There number of rows deleted should match the length of the Ids array"); 65 | } 66 | [TestMethod] 67 | public async Task With_Contains_Integer_List() 68 | { 69 | var dbContext = SetupDbContext(true); 70 | var emptyList = new List() { 1, 2, 3, 4, 5 }; 71 | var orders = dbContext.Orders.Where(o => emptyList.Contains(o.Id)); 72 | int oldTotal = orders.Count(); 73 | int rowsDeleted = await orders.DeleteFromQueryAsync(); 74 | int newTotal = orders.Count(); 75 | 76 | Assert.IsTrue(oldTotal > 0, "There must be orders in database to delete"); 77 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 78 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 79 | } 80 | [TestMethod] 81 | public async Task With_Decimal_Using_IQuerable() 82 | { 83 | var dbContext = SetupDbContext(true); 84 | var orders = dbContext.Orders.Where(o => o.Price <= 10); 85 | int oldTotal = orders.Count(); 86 | int rowsDeleted = await orders.DeleteFromQueryAsync(); 87 | int newTotal = orders.Count(); 88 | 89 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 90 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 91 | Assert.IsTrue(newTotal == 0, "Delete() Failed: must be 0 to indicate all records were deleted"); 92 | } 93 | [TestMethod] 94 | public async Task With_Decimal_Using_IEnumerable() 95 | { 96 | var dbContext = SetupDbContext(true); 97 | var orders = dbContext.Orders.Where(o => o.Price <= 10); 98 | int oldTotal = orders.Count(); 99 | int rowsDeleted = await orders.DeleteFromQueryAsync(); 100 | int newTotal = orders.Count(); 101 | 102 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 103 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 104 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 105 | } 106 | [TestMethod] 107 | public async Task With_DateTime() 108 | { 109 | var dbContext = SetupDbContext(true); 110 | int oldTotal = dbContext.Orders.Count(); 111 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 112 | int rowsToDelete = dbContext.Orders.Where(o => o.ModifiedDateTime != null && o.ModifiedDateTime >= dateTime).Count(); 113 | int rowsDeleted = await dbContext.Orders.Where(o => o.ModifiedDateTime != null && o.ModifiedDateTime >= dateTime) 114 | .DeleteFromQueryAsync(); 115 | int newTotal = dbContext.Orders.Count(); 116 | 117 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 118 | Assert.IsTrue(rowsDeleted == rowsToDelete, "The number of rows deleted must match the count of the rows that matched in the database"); 119 | Assert.IsTrue(oldTotal - newTotal == rowsDeleted, "The rows deleted must match the new count minues the old count"); 120 | } 121 | [TestMethod] 122 | public async Task With_Delete_All() 123 | { 124 | var dbContext = SetupDbContext(true); 125 | int oldTotal = dbContext.Orders.Count(); 126 | int rowsDeleted = await dbContext.Orders.DeleteFromQueryAsync(); 127 | int newTotal = dbContext.Orders.Count(); 128 | 129 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 130 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 131 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 132 | } 133 | [TestMethod] 134 | public async Task With_Different_Values() 135 | { 136 | var dbContext = SetupDbContext(true); 137 | int oldTotal = dbContext.Orders.Count(); 138 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 139 | var orders = dbContext.Orders.Where(o => o.Id == 1 && o.Active && o.ModifiedDateTime >= dateTime); 140 | int rowsToDelete = orders.Count(); 141 | int rowsDeleted = await orders.DeleteFromQueryAsync(); 142 | int newTotal = dbContext.Orders.Count(); 143 | 144 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition"); 145 | Assert.IsTrue(rowsDeleted == rowsToDelete, "The number of rows deleted must match the count of the rows that matched in the database"); 146 | Assert.IsTrue(oldTotal - newTotal == rowsDeleted, "The rows deleted must match the new count minues the old count"); 147 | } 148 | [TestMethod] 149 | public async Task With_Schema() 150 | { 151 | var dbContext = SetupDbContext(true, PopulateDataMode.Schema); 152 | int oldTotal = dbContext.ProductsWithCustomSchema.Count(); 153 | int rowsDeleted = await dbContext.ProductsWithCustomSchema.DeleteFromQueryAsync(); 154 | int newTotal = dbContext.ProductsWithCustomSchema.Count(); 155 | 156 | Assert.IsTrue(oldTotal > 0, "There must be products in database that match this condition"); 157 | Assert.IsTrue(rowsDeleted == oldTotal, "The number of rows deleted must match the count of existing rows in database"); 158 | Assert.IsTrue(newTotal == 0, "The new count must be 0 to indicate all records were deleted"); 159 | } 160 | [TestMethod] 161 | public async Task With_Transaction() 162 | { 163 | var dbContext = SetupDbContext(true); 164 | int rowsDeleted; 165 | int oldTotal = dbContext.Orders.Count(); 166 | var orders = dbContext.Orders.Where(o => o.Price <= 10); 167 | int rowsToDelete = orders.Count(); 168 | using (var transaction = dbContext.Database.BeginTransaction()) 169 | { 170 | rowsDeleted = await orders.DeleteFromQueryAsync(); 171 | transaction.Rollback(); 172 | } 173 | int newTotal = dbContext.Orders.Count(); 174 | 175 | Assert.IsTrue(oldTotal > 0, "There must be orders in database that match this condition (Price < $10)"); 176 | Assert.IsTrue(rowsDeleted == orders.Count(), "The number of rows update must match the count of rows that match the condtion (Price < $10)"); 177 | Assert.IsTrue(newTotal == oldTotal, "The new count must match the old count since the transaction was rollbacked"); 178 | } 179 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/Fetch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Entity; 3 | using System.Linq; 4 | using System.Xml.XPath; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using N.EntityFramework.Extensions.Test.Data; 7 | 8 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 9 | 10 | [TestClass] 11 | public class Fetch : DbContextExtensionsBase 12 | { 13 | [TestMethod] 14 | public void With_BulkInsert() 15 | { 16 | var dbContext = SetupDbContext(true); 17 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 18 | var orders = dbContext.Orders.Where(o => o.AddedDateTime <= dateTime); 19 | int totalOrdersToFetch = orders.Count(); 20 | int totalOrdersFetched = 0; 21 | int batchSize = 5000; 22 | orders.Fetch(result => 23 | { 24 | totalOrdersFetched += result.Results.Count(); 25 | var ordersFetched = result.Results; 26 | foreach (var orderFetched in ordersFetched) 27 | { 28 | orderFetched.Price = 75; 29 | } 30 | dbContext.BulkInsert(ordersFetched); 31 | }, options => { options.BatchSize = batchSize; }); 32 | 33 | int totalOrder = orders.Count(); 34 | int totalOrderInserted = orders.Where(o => o.Price == 75).Count(); 35 | Assert.IsTrue(totalOrdersToFetch == totalOrdersFetched, "The total number of rows fetched must match the number of rows to fetch"); 36 | Assert.IsTrue(totalOrderInserted == totalOrdersFetched, "The total number of rows updated must match the number of rows that were fetched"); 37 | Assert.IsTrue(totalOrder - totalOrdersToFetch == totalOrderInserted, "The total number of rows must match the number of rows that were updated"); 38 | } 39 | [TestMethod] 40 | public void With_BulkUpdate() 41 | { 42 | var dbContext = SetupDbContext(true); 43 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 44 | var orders = dbContext.Orders.Where(o => o.AddedDateTime <= dateTime); 45 | int totalOrdersToFetch = orders.Count(); 46 | int totalOrdersFetched = 0; 47 | int batchSize = 5000; 48 | orders.Fetch(result => 49 | { 50 | totalOrdersFetched += result.Results.Count(); 51 | var ordersFetched = result.Results; 52 | foreach (var orderFetched in ordersFetched) 53 | { 54 | orderFetched.Price = 75; 55 | } 56 | dbContext.BulkUpdate(ordersFetched); 57 | }, options => { options.BatchSize = batchSize; }); 58 | 59 | int totalOrder = orders.Count(); 60 | int totalOrderUpdated = orders.Where(o => o.Price == 75).Count(); 61 | Assert.IsTrue(totalOrdersToFetch == totalOrdersFetched, "The total number of rows fetched must match the number of rows to fetch"); 62 | Assert.IsTrue(totalOrderUpdated == totalOrdersFetched, "The total number of rows updated must match the number of rows that were fetched"); 63 | Assert.IsTrue(totalOrder == totalOrderUpdated, "The total number of rows must match the number of rows that were updated"); 64 | } 65 | [TestMethod] 66 | public void With_DateTime() 67 | { 68 | var dbContext = SetupDbContext(true); 69 | int batchSize = 1000; 70 | int batchCount = 0; 71 | int totalCount = 0; 72 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 73 | var orders = dbContext.Orders.Where(o => o.AddedDateTime <= dateTime); 74 | int expectedTotalCount = orders.Count(); 75 | int expectedBatchCount = (int)Math.Ceiling(expectedTotalCount / (decimal)batchSize); 76 | 77 | orders.Fetch(result => 78 | { 79 | batchCount++; 80 | totalCount += result.Results.Count(); 81 | Assert.IsTrue(result.Results.Count <= batchSize, "The count of results in each batch callback should less than or equal to the batchSize"); 82 | }, options => { options.BatchSize = batchSize; }); 83 | 84 | Assert.IsTrue(expectedTotalCount > 0, "There must be orders in database that match this condition"); 85 | Assert.IsTrue(expectedTotalCount == totalCount, "The total number of rows fetched must match the count of existing rows in database"); 86 | Assert.IsTrue(expectedBatchCount == batchCount, "The total number of batches fetched must match what is expected"); 87 | } 88 | [TestMethod] 89 | public void With_Decimal() 90 | { 91 | var dbContext = SetupDbContext(true); 92 | int batchSize = 1000; 93 | int batchCount = 0; 94 | int totalCount = 0; 95 | var orders = dbContext.Orders.Where(o => o.Price < 10M); 96 | int expectedTotalCount = orders.Count(); 97 | int expectedBatchCount = (int)Math.Ceiling(expectedTotalCount / (decimal)batchSize); 98 | 99 | orders.Fetch(result => 100 | { 101 | batchCount++; 102 | totalCount += result.Results.Count(); 103 | Assert.IsTrue(result.Results.Count <= batchSize, "The count of results in each batch callback should less than or equal to the batchSize"); 104 | }, options => { options.BatchSize = batchSize; }); 105 | 106 | Assert.IsTrue(expectedTotalCount > 0, "There must be orders in database that match this condition"); 107 | Assert.IsTrue(expectedTotalCount == totalCount, "The total number of rows fetched must match the count of existing rows in database"); 108 | Assert.IsTrue(expectedBatchCount == batchCount, "The total number of batches fetched must match what is expected"); 109 | } 110 | [TestMethod] 111 | public void With_Options_IgnoreColumns() 112 | { 113 | var dbContext = SetupDbContext(true); 114 | int batchSize = 1000; 115 | int batchCount = 0; 116 | int totalCount = 0; 117 | var orders = dbContext.Orders.Where(o => o.Price < 10M); 118 | int expectedTotalCount = orders.Count(); 119 | int expectedBatchCount = (int)Math.Ceiling(expectedTotalCount / (decimal)batchSize); 120 | 121 | orders.Fetch(result => 122 | { 123 | batchCount++; 124 | totalCount += result.Results.Count(); 125 | bool isAllExternalIdNull = !result.Results.Any(o => o.ExternalId != null); 126 | Assert.IsTrue(isAllExternalIdNull, "All records should have ExternalId equal to NULL since it was not loaded."); 127 | Assert.IsTrue(result.Results.Count <= batchSize, "The count of results in each batch callback should less than or equal to the batchSize"); 128 | }, options => { options.BatchSize = batchSize; options.IgnoreColumns = s => new { s.ExternalId }; }); 129 | 130 | Assert.IsTrue(expectedTotalCount > 0, "There must be orders in database that match this condition"); 131 | Assert.IsTrue(expectedTotalCount == totalCount, "The total number of rows fetched must match the count of existing rows in database"); 132 | Assert.IsTrue(expectedBatchCount == batchCount, "The total number of batches fetched must match what is expected"); 133 | } 134 | [TestMethod] 135 | public void With_Options_InputColumns() 136 | { 137 | var dbContext = SetupDbContext(true); 138 | int batchSize = 1000; 139 | int batchCount = 0; 140 | int totalCount = 0; 141 | var orders = dbContext.Orders.Where(o => o.Price < 10M); 142 | int expectedTotalCount = orders.Count(); 143 | int expectedBatchCount = (int)Math.Ceiling(expectedTotalCount / (decimal)batchSize); 144 | 145 | orders.Fetch(result => 146 | { 147 | batchCount++; 148 | totalCount += result.Results.Count(); 149 | bool isAllExternalIdNull = !result.Results.Any(o => o.ExternalId != null); 150 | Assert.IsTrue(isAllExternalIdNull, "All records should have ExternalId equal to NULL since it was not loaded."); 151 | Assert.IsTrue(result.Results.Count <= batchSize, "The count of results in each batch callback should less than or equal to the batchSize"); 152 | }, options => { options.BatchSize = batchSize; options.InputColumns = s => new { s.Id, s.Price }; }); 153 | 154 | Assert.IsTrue(expectedTotalCount > 0, "There must be orders in database that match this condition"); 155 | Assert.IsTrue(expectedTotalCount == totalCount, "The total number of rows fetched must match the count of existing rows in database"); 156 | Assert.IsTrue(expectedBatchCount == batchCount, "The total number of batches fetched must match what is expected"); 157 | } 158 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/FetchAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 7 | 8 | [TestClass] 9 | public class FetchAsync : DbContextExtensionsBase 10 | { 11 | [TestMethod] 12 | public async Task With_BulkInsert() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 16 | var orders = dbContext.Orders.Where(o => o.AddedDateTime <= dateTime); 17 | int totalOrdersToFetch = orders.Count(); 18 | int totalOrdersFetched = 0; 19 | int batchSize = 5000; 20 | await orders.FetchAsync(async result => 21 | { 22 | totalOrdersFetched += result.Results.Count(); 23 | var ordersFetched = result.Results; 24 | foreach (var orderFetched in ordersFetched) 25 | { 26 | orderFetched.Price = 75; 27 | } 28 | await dbContext.BulkInsertAsync(ordersFetched); 29 | }, options => { options.BatchSize = batchSize; }); 30 | 31 | int totalOrder = orders.Count(); 32 | int totalOrderInserted = orders.Where(o => o.Price == 75).Count(); 33 | Assert.IsTrue(totalOrdersToFetch == totalOrdersFetched, "The total number of rows fetched must match the number of rows to fetch"); 34 | Assert.IsTrue(totalOrderInserted == totalOrdersFetched, "The total number of rows updated must match the number of rows that were fetched"); 35 | Assert.IsTrue(totalOrder - totalOrdersToFetch == totalOrderInserted, "The total number of rows must match the number of rows that were updated"); 36 | } 37 | [TestMethod] 38 | public async Task With_BulkUpdate() 39 | { 40 | var dbContext = SetupDbContext(true); 41 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 42 | var orders = dbContext.Orders.Where(o => o.AddedDateTime <= dateTime); 43 | int totalOrdersToFetch = orders.Count(); 44 | int totalOrdersFetched = 0; 45 | int batchSize = 5000; 46 | await orders.FetchAsync(async result => 47 | { 48 | totalOrdersFetched += result.Results.Count(); 49 | var ordersFetched = result.Results; 50 | foreach (var orderFetched in ordersFetched) 51 | { 52 | orderFetched.Price = 75; 53 | } 54 | await dbContext.BulkUpdateAsync(ordersFetched); 55 | }, options => { options.BatchSize = batchSize; }); 56 | 57 | int totalOrder = orders.Count(); 58 | int totalOrderUpdated = orders.Where(o => o.Price == 75).Count(); 59 | Assert.IsTrue(totalOrdersToFetch == totalOrdersFetched, "The total number of rows fetched must match the number of rows to fetch"); 60 | Assert.IsTrue(totalOrderUpdated == totalOrdersFetched, "The total number of rows updated must match the number of rows that were fetched"); 61 | Assert.IsTrue(totalOrder == totalOrderUpdated, "The total number of rows must match the number of rows that were updated"); 62 | } 63 | [TestMethod] 64 | public async Task With_DateTime() 65 | { 66 | var dbContext = SetupDbContext(true); 67 | int batchSize = 1000; 68 | int batchCount = 0; 69 | int totalCount = 0; 70 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 71 | var orders = dbContext.Orders.Where(o => o.AddedDateTime <= dateTime); 72 | int expectedTotalCount = orders.Count(); 73 | int expectedBatchCount = (int)Math.Ceiling(expectedTotalCount / (decimal)batchSize); 74 | 75 | await orders.FetchAsync(async result => 76 | { 77 | batchCount++; 78 | totalCount += result.Results.Count(); 79 | Assert.IsTrue(result.Results.Count <= batchSize, "The count of results in each batch callback should less than or equal to the batchSize"); 80 | await Task.FromResult(result); 81 | }, options => { options.BatchSize = batchSize; }); 82 | 83 | Assert.IsTrue(expectedTotalCount > 0, "There must be orders in database that match this condition"); 84 | Assert.IsTrue(expectedTotalCount == totalCount, "The total number of rows fetched must match the count of existing rows in database"); 85 | Assert.IsTrue(expectedBatchCount == batchCount, "The total number of batches fetched must match what is expected"); 86 | } 87 | [TestMethod] 88 | public async Task With_Decimal() 89 | { 90 | var dbContext = SetupDbContext(true); 91 | int batchSize = 1000; 92 | int batchCount = 0; 93 | int totalCount = 0; 94 | var orders = dbContext.Orders.Where(o => o.Price < 10M); 95 | int expectedTotalCount = orders.Count(); 96 | int expectedBatchCount = (int)Math.Ceiling(expectedTotalCount / (decimal)batchSize); 97 | 98 | await orders.FetchAsync(async result => 99 | { 100 | batchCount++; 101 | totalCount += result.Results.Count(); 102 | Assert.IsTrue(result.Results.Count <= batchSize, "The count of results in each batch callback should less than or equal to the batchSize"); 103 | await Task.FromResult(result); 104 | }, options => { options.BatchSize = batchSize; }); 105 | 106 | Assert.IsTrue(expectedTotalCount > 0, "There must be orders in database that match this condition"); 107 | Assert.IsTrue(expectedTotalCount == totalCount, "The total number of rows fetched must match the count of existing rows in database"); 108 | Assert.IsTrue(expectedBatchCount == batchCount, "The total number of batches fetched must match what is expected"); 109 | } 110 | [TestMethod] 111 | public async Task With_Options_IgnoreColumns() 112 | { 113 | var dbContext = SetupDbContext(true); 114 | int batchSize = 1000; 115 | int batchCount = 0; 116 | int totalCount = 0; 117 | var orders = dbContext.Orders.Where(o => o.Price < 10M); 118 | int expectedTotalCount = orders.Count(); 119 | int expectedBatchCount = (int)Math.Ceiling(expectedTotalCount / (decimal)batchSize); 120 | 121 | await orders.FetchAsync(async result => 122 | { 123 | batchCount++; 124 | totalCount += result.Results.Count(); 125 | bool isAllExternalIdNull = !result.Results.Any(o => o.ExternalId != null); 126 | Assert.IsTrue(isAllExternalIdNull, "All records should have ExternalId equal to NULL since it was not loaded."); 127 | Assert.IsTrue(result.Results.Count <= batchSize, "The count of results in each batch callback should less than or equal to the batchSize"); 128 | await Task.FromResult(result); 129 | }, options => { options.BatchSize = batchSize; options.IgnoreColumns = s => new { s.ExternalId }; }); 130 | 131 | Assert.IsTrue(expectedTotalCount > 0, "There must be orders in database that match this condition"); 132 | Assert.IsTrue(expectedTotalCount == totalCount, "The total number of rows fetched must match the count of existing rows in database"); 133 | Assert.IsTrue(expectedBatchCount == batchCount, "The total number of batches fetched must match what is expected"); 134 | } 135 | [TestMethod] 136 | public async Task With_Options_InputColumns() 137 | { 138 | var dbContext = SetupDbContext(true); 139 | int batchSize = 1000; 140 | int batchCount = 0; 141 | int totalCount = 0; 142 | var orders = dbContext.Orders.Where(o => o.Price < 10M); 143 | int expectedTotalCount = orders.Count(); 144 | int expectedBatchCount = (int)Math.Ceiling(expectedTotalCount / (decimal)batchSize); 145 | 146 | await orders.FetchAsync(async result => 147 | { 148 | batchCount++; 149 | totalCount += result.Results.Count(); 150 | bool isAllExternalIdNull = !result.Results.Any(o => o.ExternalId != null); 151 | Assert.IsTrue(isAllExternalIdNull, "All records should have ExternalId equal to NULL since it was not loaded."); 152 | Assert.IsTrue(result.Results.Count <= batchSize, "The count of results in each batch callback should less than or equal to the batchSize"); 153 | await Task.FromResult(result); 154 | }, options => { options.BatchSize = batchSize; options.InputColumns = s => new { s.Id, s.Price }; }); 155 | 156 | Assert.IsTrue(expectedTotalCount > 0, "There must be orders in database that match this condition"); 157 | Assert.IsTrue(expectedTotalCount == totalCount, "The total number of rows fetched must match the count of existing rows in database"); 158 | Assert.IsTrue(expectedBatchCount == batchCount, "The total number of batches fetched must match what is expected"); 159 | } 160 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/InsertFromQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 7 | 8 | [TestClass] 9 | public class InsertFromQuery : DbContextExtensionsBase 10 | { 11 | [TestMethod] 12 | public void With_DateTime_Value() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | string tableName = "OrdersLast30Days"; 16 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 17 | int oldTotal = dbContext.Orders.Count(); 18 | 19 | var orders = dbContext.Orders.Where(o => o.AddedDateTime >= dateTime); 20 | int oldSourceTotal = orders.Count(); 21 | int rowsInserted = orders.InsertFromQuery(tableName, 22 | o => new { o.Id, o.ExternalId, o.Price, o.AddedDateTime, o.ModifiedDateTime, o.Active }); 23 | int newSourceTotal = orders.Count(); 24 | int newTargetTotal = orders.UsingTable(tableName).Count(); 25 | 26 | Assert.IsTrue(oldTotal > oldSourceTotal, "The total should be greater then the number of rows selected from the source table"); 27 | Assert.IsTrue(oldSourceTotal > 0, "There should be existing data in the source table"); 28 | Assert.IsTrue(oldSourceTotal == newSourceTotal, "There should not be any change in the count of rows in the source table"); 29 | Assert.IsTrue(rowsInserted == oldSourceTotal, "The number of records inserted must match the count of the source table"); 30 | Assert.IsTrue(rowsInserted == newTargetTotal, "The different in count in the target table before and after the insert must match the total row inserted"); 31 | } 32 | [TestMethod] 33 | public void With_Decimal_Value() 34 | { 35 | var dbContext = SetupDbContext(true); 36 | string tableName = "OrdersUnderTen"; 37 | var orders = dbContext.Orders.Where(o => o.Price < 10M); 38 | int oldSourceTotal = orders.Count(); 39 | int rowsInserted = dbContext.Orders.Where(o => o.Price < 10M).InsertFromQuery(tableName, o => new { o.Id, o.Price, o.AddedDateTime, o.Active }); 40 | int newSourceTotal = orders.Count(); 41 | int newTargetTotal = orders.UsingTable(tableName).Count(); 42 | 43 | Assert.IsTrue(oldSourceTotal > 0, "There should be existing data in the source table"); 44 | Assert.IsTrue(oldSourceTotal == newSourceTotal, "There should not be any change in the count of rows in the source table"); 45 | Assert.IsTrue(rowsInserted == oldSourceTotal, "The number of records inserted must match the count of the source table"); 46 | Assert.IsTrue(rowsInserted == newTargetTotal, "The different in count in the target table before and after the insert must match the total row inserted"); 47 | } 48 | [TestMethod] 49 | public void With_Schema() 50 | { 51 | var dbContext = SetupDbContext(true, PopulateDataMode.Schema); 52 | string tableName = "top.ProductsUnderTen"; 53 | var products = dbContext.ProductsWithCustomSchema.Where(o => o.Price < 10M); 54 | int oldSourceTotal = products.Count(); 55 | int rowsInserted = dbContext.ProductsWithCustomSchema.Where(o => o.Price < 10M).InsertFromQuery(tableName, o => new { o.Id, o.Price }); 56 | int newSourceTotal = products.Count(); 57 | int newTargetTotal = products.UsingTable(tableName).Count(); 58 | 59 | Assert.IsTrue(oldSourceTotal > 0, "There should be existing data in the source table"); 60 | Assert.IsTrue(oldSourceTotal == newSourceTotal, "There should not be any change in the count of rows in the source table"); 61 | Assert.IsTrue(rowsInserted == oldSourceTotal, "The number of records inserted must match the count of the source table"); 62 | Assert.IsTrue(rowsInserted == newTargetTotal, "The different in count in the target table before and after the insert must match the total row inserted"); 63 | } 64 | [TestMethod] 65 | public void With_Transaction() 66 | { 67 | var dbContext = SetupDbContext(true); 68 | string tableName = "OrdersUnderTen"; 69 | int rowsInserted; 70 | bool tableExistsBefore, tableExistsAfter; 71 | int oldSourceTotal = dbContext.Orders.Where(o => o.Price < 10M).Count(); 72 | using (var transaction = dbContext.Database.BeginTransaction()) 73 | { 74 | rowsInserted = dbContext.Orders.Where(o => o.Price < 10M).InsertFromQuery(tableName, o => new { o.Price, o.Id, o.AddedDateTime, o.Active }); 75 | tableExistsBefore = dbContext.Database.TableExists(tableName); 76 | transaction.Rollback(); 77 | } 78 | tableExistsAfter = dbContext.Database.TableExists(tableName); 79 | int newSourceTotal = dbContext.Orders.Where(o => o.Price < 10M).Count(); 80 | 81 | Assert.IsTrue(oldSourceTotal > 0, "There must be orders in database that match this condition (Price < $10)"); 82 | Assert.IsTrue(rowsInserted == oldSourceTotal, "The number of rows update must match the count of rows that match the condtion (Price < $10)"); 83 | Assert.IsTrue(newSourceTotal == oldSourceTotal, "The new count must match the old count since the transaction was rollbacked"); 84 | Assert.IsTrue(tableExistsBefore, string.Format("Table {0} should exist before transaction rollback", tableName)); 85 | Assert.IsFalse(tableExistsAfter, string.Format("Table {0} should not exist after transaction rollback", tableName)); 86 | } 87 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/InsertFromQueryAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 7 | 8 | [TestClass] 9 | public class InsertFromQueryAsync : DbContextExtensionsBase 10 | { 11 | [TestMethod] 12 | public async Task With_DateTime_Value() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | string tableName = "OrdersLast30Days"; 16 | DateTime dateTime = dbContext.Orders.Max(o => o.AddedDateTime).AddDays(-30); 17 | int oldTotal = dbContext.Orders.Count(); 18 | 19 | var orders = dbContext.Orders.Where(o => o.AddedDateTime >= dateTime); 20 | int oldSourceTotal = orders.Count(); 21 | int rowsInserted = await orders.InsertFromQueryAsync(tableName, 22 | o => new { o.Id, o.ExternalId, o.Price, o.AddedDateTime, o.ModifiedDateTime, o.Active }); 23 | int newSourceTotal = orders.Count(); 24 | int newTargetTotal = orders.UsingTable(tableName).Count(); 25 | 26 | Assert.IsTrue(oldTotal > oldSourceTotal, "The total should be greater then the number of rows selected from the source table"); 27 | Assert.IsTrue(oldSourceTotal > 0, "There should be existing data in the source table"); 28 | Assert.IsTrue(oldSourceTotal == newSourceTotal, "There should not be any change in the count of rows in the source table"); 29 | Assert.IsTrue(rowsInserted == oldSourceTotal, "The number of records inserted must match the count of the source table"); 30 | Assert.IsTrue(rowsInserted == newTargetTotal, "The different in count in the target table before and after the insert must match the total row inserted"); 31 | } 32 | [TestMethod] 33 | public async Task With_Decimal_Value() 34 | { 35 | var dbContext = SetupDbContext(true); 36 | string tableName = "OrdersUnderTen"; 37 | var orders = dbContext.Orders.Where(o => o.Price < 10M); 38 | int oldSourceTotal = orders.Count(); 39 | int rowsInserted = await dbContext.Orders.Where(o => o.Price < 10M).InsertFromQueryAsync(tableName, o => new { o.Id, o.Price, o.AddedDateTime, o.Active }); 40 | int newSourceTotal = orders.Count(); 41 | int newTargetTotal = orders.UsingTable(tableName).Count(); 42 | 43 | Assert.IsTrue(oldSourceTotal > 0, "There should be existing data in the source table"); 44 | Assert.IsTrue(oldSourceTotal == newSourceTotal, "There should not be any change in the count of rows in the source table"); 45 | Assert.IsTrue(rowsInserted == oldSourceTotal, "The number of records inserted must match the count of the source table"); 46 | Assert.IsTrue(rowsInserted == newTargetTotal, "The different in count in the target table before and after the insert must match the total row inserted"); 47 | } 48 | [TestMethod] 49 | public async Task With_Schema() 50 | { 51 | var dbContext = SetupDbContext(true, PopulateDataMode.Schema); 52 | string tableName = "top.ProductsUnderTen"; 53 | var products = dbContext.ProductsWithCustomSchema.Where(o => o.Price < 10M); 54 | int oldSourceTotal = products.Count(); 55 | int rowsInserted = await dbContext.ProductsWithCustomSchema.Where(o => o.Price < 10M).InsertFromQueryAsync(tableName, o => new { o.Id, o.Price }); 56 | int newSourceTotal = products.Count(); 57 | int newTargetTotal = products.UsingTable(tableName).Count(); 58 | 59 | Assert.IsTrue(oldSourceTotal > 0, "There should be existing data in the source table"); 60 | Assert.IsTrue(oldSourceTotal == newSourceTotal, "There should not be any change in the count of rows in the source table"); 61 | Assert.IsTrue(rowsInserted == oldSourceTotal, "The number of records inserted must match the count of the source table"); 62 | Assert.IsTrue(rowsInserted == newTargetTotal, "The different in count in the target table before and after the insert must match the total row inserted"); 63 | } 64 | [TestMethod] 65 | public async Task With_Transaction() 66 | { 67 | var dbContext = SetupDbContext(true); 68 | string tableName = "OrdersUnderTen"; 69 | int rowsInserted; 70 | bool tableExistsBefore, tableExistsAfter; 71 | int oldSourceTotal = dbContext.Orders.Where(o => o.Price < 10M).Count(); 72 | using (var transaction = dbContext.Database.BeginTransaction()) 73 | { 74 | rowsInserted = await dbContext.Orders.Where(o => o.Price < 10M).InsertFromQueryAsync(tableName, o => new { o.Price, o.Id, o.AddedDateTime, o.Active }); 75 | tableExistsBefore = dbContext.Database.TableExists(tableName); 76 | transaction.Rollback(); 77 | } 78 | tableExistsAfter = dbContext.Database.TableExists(tableName); 79 | int newSourceTotal = dbContext.Orders.Where(o => o.Price < 10M).Count(); 80 | 81 | Assert.IsTrue(oldSourceTotal > 0, "There must be orders in database that match this condition (Price < $10)"); 82 | Assert.IsTrue(rowsInserted == oldSourceTotal, "The number of rows update must match the count of rows that match the condtion (Price < $10)"); 83 | Assert.IsTrue(newSourceTotal == oldSourceTotal, "The new count must match the old count since the transaction was rollbacked"); 84 | Assert.IsTrue(tableExistsBefore, string.Format("Table {0} should exist before transaction rollback", tableName)); 85 | Assert.IsFalse(tableExistsAfter, string.Format("Table {0} should not exist after transaction rollback", tableName)); 86 | } 87 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/QueryToCsvFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 6 | 7 | [TestClass] 8 | public class QueryToCsvFile : DbContextExtensionsBase 9 | { 10 | [TestMethod] 11 | public void With_Default_Options() 12 | { 13 | var dbContext = SetupDbContext(true); 14 | var query = dbContext.Orders.Where(o => o.Price < 10M); 15 | int count = query.Count(); 16 | var queryToCsvFileResult = query.QueryToCsvFile("QueryToCsvFile-Test.csv"); 17 | 18 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 19 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 20 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count + 1, "The total number of rows written to the file should match the count from the database plus the header row"); 21 | } 22 | [TestMethod] 23 | public void With_Options_ColumnDelimiter_TextQualifer_HeaderRow() 24 | { 25 | var dbContext = SetupDbContext(true); 26 | var query = dbContext.Orders.Where(o => o.Price < 10M); 27 | int count = query.Count(); 28 | var queryToCsvFileResult = query.QueryToCsvFile("QueryToCsvFile_Options_ColumnDelimiter_TextQualifer_HeaderRow-Test.csv", options => { options.ColumnDelimiter = "|"; options.TextQualifer = "\""; options.IncludeHeaderRow = false; }); 29 | 30 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 31 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 32 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count, "The total number of rows written to the file should match the count from the database without any header row"); 33 | } 34 | [TestMethod] 35 | public void Using_FileStream() 36 | { 37 | var dbContext = SetupDbContext(true); 38 | var query = dbContext.Orders.Where(o => o.Price < 10M); 39 | int count = query.Count(); 40 | var fileStream = File.Create("QueryToCsvFile_Stream-Test.csv"); 41 | var queryToCsvFileResult = query.QueryToCsvFile(fileStream); 42 | 43 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 44 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 45 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count + 1, "The total number of rows written to the file should match the count from the database plus the header row"); 46 | } 47 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbContextExtensions/QueryToCsvFileAsync.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DbContextExtensions; 7 | 8 | [TestClass] 9 | public class QueryToCsvFileAsync : DbContextExtensionsBase 10 | { 11 | [TestMethod] 12 | public async Task With_Default_Options() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | var query = dbContext.Orders.Where(o => o.Price < 10M); 16 | int count = query.Count(); 17 | var queryToCsvFileResult = await query.QueryToCsvFileAsync("QueryToCsvFile-Test.csv"); 18 | 19 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 20 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 21 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count + 1, "The total number of rows written to the file should match the count from the database plus the header row"); 22 | } 23 | [TestMethod] 24 | public async Task With_Options_ColumnDelimiter_TextQualifer_HeaderRow() 25 | { 26 | var dbContext = SetupDbContext(true); 27 | var query = dbContext.Orders.Where(o => o.Price < 10M); 28 | int count = query.Count(); 29 | var queryToCsvFileResult = await query.QueryToCsvFileAsync("QueryToCsvFile_Options_ColumnDelimiter_TextQualifer_HeaderRow-Test.csv", options => { options.ColumnDelimiter = "|"; options.TextQualifer = "\""; options.IncludeHeaderRow = false; }); 30 | 31 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 32 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 33 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count, "The total number of rows written to the file should match the count from the database without any header row"); 34 | } 35 | [TestMethod] 36 | public async Task Using_FileStream() 37 | { 38 | var dbContext = SetupDbContext(true); 39 | var query = dbContext.Orders.Where(o => o.Price < 10M); 40 | int count = query.Count(); 41 | var fileStream = File.Create("QueryToCsvFile_Stream-Test.csv"); 42 | var queryToCsvFileResult = await query.QueryToCsvFileAsync(fileStream); 43 | 44 | Assert.IsTrue(count > 0, "There should be existing data in the source table"); 45 | Assert.IsTrue(queryToCsvFileResult.DataRowCount == count, "The number of data rows written to the file should match the count from the database"); 46 | Assert.IsTrue(queryToCsvFileResult.TotalRowCount == count + 1, "The total number of rows written to the file should match the count from the database plus the header row"); 47 | } 48 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbSetExtensions/Clear.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using N.EntityFramework.Extensions.Test.DbContextExtensions; 4 | 5 | namespace N.EntityFramework.Extensions.Test.DbSetExtensions; 6 | 7 | [TestClass] 8 | public class Clear : DbContextExtensionsBase 9 | { 10 | [TestMethod] 11 | public void Using_Dbset() 12 | { 13 | var dbContext = SetupDbContext(true); 14 | int oldOrdersCount = dbContext.Orders.Count(); 15 | dbContext.Orders.Clear(); 16 | int newOrdersCount = dbContext.Orders.Count(); 17 | 18 | Assert.IsTrue(oldOrdersCount > 0, "Orders table should have data"); 19 | Assert.IsTrue(newOrdersCount == 0, "Order table should be empty after truncating"); 20 | } 21 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbSetExtensions/ClearAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using N.EntityFramework.Extensions.Test.DbContextExtensions; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DbSetExtensions; 7 | 8 | [TestClass] 9 | public class ClearAsync : DbContextExtensionsBase 10 | { 11 | [TestMethod] 12 | public async Task Using_Dbset() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | int oldOrdersCount = dbContext.Orders.Count(); 16 | await dbContext.Orders.ClearAsync(); 17 | int newOrdersCount = dbContext.Orders.Count(); 18 | 19 | Assert.IsTrue(oldOrdersCount > 0, "Orders table should have data"); 20 | Assert.IsTrue(newOrdersCount == 0, "Order table should be empty after truncating"); 21 | } 22 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbSetExtensions/Truncate.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using N.EntityFramework.Extensions.Test.DbContextExtensions; 4 | 5 | namespace N.EntityFramework.Extensions.Test.DbSetExtensions; 6 | 7 | [TestClass] 8 | public class Truncate : DbContextExtensionsBase 9 | { 10 | [TestMethod] 11 | public void Using_Dbset() 12 | { 13 | var dbContext = SetupDbContext(true); 14 | int oldOrdersCount = dbContext.Orders.Count(); 15 | dbContext.Orders.Truncate(); 16 | int newOrdersCount = dbContext.Orders.Count(); 17 | 18 | Assert.IsTrue(oldOrdersCount > 0, "Orders table should have data"); 19 | Assert.IsTrue(newOrdersCount == 0, "Order table should be empty after truncating"); 20 | } 21 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/DbSetExtensions/TruncateAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using N.EntityFramework.Extensions.Test.DbContextExtensions; 5 | 6 | namespace N.EntityFramework.Extensions.Test.DbSetExtensions; 7 | 8 | [TestClass] 9 | public class TruncateAsync : DbContextExtensionsBase 10 | { 11 | [TestMethod] 12 | public async Task Using_Dbset() 13 | { 14 | var dbContext = SetupDbContext(true); 15 | int oldOrdersCount = dbContext.Orders.Count(); 16 | await dbContext.Orders.TruncateAsync(); 17 | int newOrdersCount = dbContext.Orders.Count(); 18 | 19 | Assert.IsTrue(oldOrdersCount > 0, "Orders table should have data"); 20 | Assert.IsTrue(newOrdersCount == 0, "Order table should be empty after truncating"); 21 | } 22 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Migrations/202406080146394_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace N.EntityFramework.Extensions.Test.Data 3 | { 4 | using System.CodeDom.Compiler; 5 | using System.Data.Entity.Migrations; 6 | using System.Data.Entity.Migrations.Infrastructure; 7 | using System.Resources; 8 | 9 | [GeneratedCode("EntityFramework.Migrations", "6.4.4")] 10 | public sealed partial class Initial : IMigrationMetadata 11 | { 12 | private readonly ResourceManager Resources = new ResourceManager(typeof(Initial)); 13 | 14 | string IMigrationMetadata.Id 15 | { 16 | get { return "202406080146394_Initial"; } 17 | } 18 | 19 | string IMigrationMetadata.Source 20 | { 21 | get { return Resources.GetString("Source"); } 22 | } 23 | 24 | string IMigrationMetadata.Target 25 | { 26 | get { return Resources.GetString("Target"); } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Migrations/202406080146394_Initial.cs: -------------------------------------------------------------------------------- 1 | namespace N.EntityFramework.Extensions.Test.Data; 2 | 3 | using System; 4 | using System.Data.Entity.Migrations; 5 | 6 | public partial class Initial : DbMigration 7 | { 8 | public override void Up() 9 | { 10 | Sql("CREATE TRIGGER trgProductWithTriggers\r\nON ProductWithTriggers\r\nFOR INSERT, UPDATE, DELETE\r\nAS\r\nBEGIN\r\n" + 11 | " PRINT 1 END"); 12 | } 13 | 14 | public override void Down() 15 | { 16 | } 17 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/N.EntityFramework.Extensions.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("Test")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("Test")] 10 | [assembly: AssemblyCopyright("Copyright © 2020")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("a52cdeff-f507-4ea2-b5b1-ae46a3afce95")] 17 | 18 | // [assembly: AssemblyVersion("1.0.*")] 19 | [assembly: AssemblyVersion("1.0.0.0")] 20 | [assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /N.EntityFramework.Extensions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30225.117 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "N.EntityFramework.Extensions", "N.EntityFramework.Extensions\N.EntityFramework.Extensions.csproj", "{EBFA8667-BB63-41C0-A5BA-E41CA2B553E8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "N.EntityFramework.Extensions.Test", "N.EntityFramework.Extensions.Test\N.EntityFramework.Extensions.Test.csproj", "{A52CDEFF-F507-4EA2-B5B1-AE46A3AFCE95}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {EBFA8667-BB63-41C0-A5BA-E41CA2B553E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {EBFA8667-BB63-41C0-A5BA-E41CA2B553E8}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {EBFA8667-BB63-41C0-A5BA-E41CA2B553E8}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {EBFA8667-BB63-41C0-A5BA-E41CA2B553E8}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {A52CDEFF-F507-4EA2-B5B1-AE46A3AFCE95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {A52CDEFF-F507-4EA2-B5B1-AE46A3AFCE95}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {A52CDEFF-F507-4EA2-B5B1-AE46A3AFCE95}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {A52CDEFF-F507-4EA2-B5B1-AE46A3AFCE95}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {68C5AD77-58F3-4E3F-8238-73ADAB7059C2} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Common/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace N.EntityFramework.Extensions.Common 2 | { 3 | public static class Constants 4 | { 5 | public readonly static string InternalId_ColumnName = "_be_xx_id"; 6 | } 7 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkDeleteOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace N.EntityFramework.Extensions 5 | { 6 | public class BulkDeleteOptions : BulkOptions 7 | { 8 | public Expression> DeleteOnCondition { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkFetchOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace N.EntityFramework.Extensions 5 | { 6 | public class BulkFetchOptions : BulkOptions 7 | { 8 | public Expression> IgnoreColumns { get; set; } 9 | public Expression> InputColumns { get; set; } 10 | public Expression> JoinOnCondition { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkInsertOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | 5 | namespace N.EntityFramework.Extensions 6 | { 7 | public class BulkInsertOptions : BulkOptions 8 | { 9 | public Expression> IgnoreColumns { get; set; } 10 | public Expression> InputColumns { get; set; } 11 | public bool AutoMapOutput { get; set; } 12 | public bool KeepIdentity { get; set; } 13 | public bool InsertIfNotExists { get; set; } 14 | public Expression> InsertOnCondition { get; set; } 15 | 16 | 17 | public string[] GetInputColumns() 18 | { 19 | return this.InputColumns == null ? null : this.InputColumns.Body.Type.GetProperties().Select(o => o.Name).ToArray(); 20 | } 21 | 22 | public BulkInsertOptions() 23 | { 24 | this.AutoMapOutput = true; 25 | this.InsertIfNotExists = false; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkInsertResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace N.EntityFramework.Extensions 5 | { 6 | internal class BulkInsertResult 7 | { 8 | internal int RowsAffected { get; set; } 9 | internal Dictionary EntityMap { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkMergeOption.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace N.EntityFramework.Extensions 7 | { 8 | public class BulkMergeOptions : BulkOptions 9 | { 10 | public Expression> MergeOnCondition { get; set; } 11 | //public Func NotMatchedBySourceCondition { get; set; } 12 | // public Func NotMatchedByTargetCondition { get; set; } 13 | //public Func MatchedCondition { get; set; } 14 | public Expression> IgnoreColumnsOnInsert { get; set; } 15 | public Expression> IgnoreColumnsOnUpdate { get; set; } 16 | public bool AutoMapOutput { get; set; } 17 | internal bool DeleteIfNotMatched { get; set; } 18 | 19 | public BulkMergeOptions() 20 | { 21 | this.AutoMapOutput = true; 22 | } 23 | public List GetIgnoreColumnsOnInsert() 24 | { 25 | return this.IgnoreColumnsOnInsert == null ? new List() : this.IgnoreColumnsOnInsert.Body.Type.GetProperties().Select(o => o.Name).ToList(); 26 | } 27 | public List GetIgnoreColumnsOnUpdate() 28 | { 29 | return this.IgnoreColumnsOnUpdate == null ? new List() : this.IgnoreColumnsOnUpdate.Body.Type.GetProperties().Select(o => o.Name).ToList(); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkMergeOutputRow.cs: -------------------------------------------------------------------------------- 1 | namespace N.EntityFramework.Extensions 2 | { 3 | public class BulkMergeOutputRow 4 | { 5 | public string Action { get; set; } 6 | //public T Item { get; set; } 7 | 8 | public BulkMergeOutputRow(string action) 9 | { 10 | this.Action = action; 11 | //this.Item = item; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkMergeResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace N.EntityFramework.Extensions 4 | { 5 | public class BulkMergeResult 6 | { 7 | public IEnumerable> Output { get; set; } 8 | public int RowsAffected { get; set; } 9 | internal virtual int RowsDeleted { get; set; } 10 | public int RowsInserted { get; internal set; } 11 | public int RowsUpdated { get; internal set; } 12 | 13 | } 14 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Entity; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using N.EntityFramework.Extensions.Enums; 8 | 9 | namespace N.EntityFramework.Extensions 10 | { 11 | public class BulkOptions 12 | { 13 | public int BatchSize { get; set; } 14 | public bool UsePermanentTable { get; set; } 15 | public int? CommandTimeout { get; set; } 16 | internal TransactionalBehavior TransactionalBehavior { get; set; } 17 | internal ConnectionBehavior ConnectionBehavior { get; set; } 18 | internal Type ClrType { get; set; } 19 | 20 | public BulkOptions() 21 | { 22 | BatchSize = 1000; 23 | TransactionalBehavior = TransactionalBehavior.EnsureTransaction; 24 | ConnectionBehavior = ConnectionBehavior.Default; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkQueryResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace N.EntityFramework.Extensions 4 | { 5 | public class BulkQueryResult 6 | { 7 | public IEnumerable Results { get; internal set; } 8 | public IEnumerable Columns { get; internal set; } 9 | public int RowsAffected { get; internal set; } 10 | } 11 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkSyncOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace N.EntityFramework.Extensions 7 | { 8 | public class BulkSyncOptions : BulkMergeOptions 9 | { 10 | public BulkSyncOptions() 11 | { 12 | this.DeleteIfNotMatched = true; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkSyncResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace N.EntityFramework.Extensions 4 | { 5 | public class BulkSyncResult : BulkMergeResult 6 | { 7 | public new int RowsDeleted { get; set; } 8 | public static BulkSyncResult Map(BulkMergeResult result) 9 | { 10 | return new BulkSyncResult() 11 | { 12 | Output = result.Output, 13 | RowsAffected = result.RowsAffected, 14 | RowsDeleted = result.RowsDeleted, 15 | RowsInserted = result.RowsInserted, 16 | RowsUpdated = result.RowsUpdated 17 | }; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/BulkUpdateOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace N.EntityFramework.Extensions 5 | { 6 | public class BulkUpdateOptions : BulkOptions 7 | { 8 | public Expression> InputColumns { get; set; } 9 | public Expression> IgnoreColumns { get; set; } 10 | [Obsolete("BulkUpdateOptions.IgnoreColumnsOnUpdate has been replaced by IgnoreColumns.")] 11 | public Expression> IgnoreColumnsOnUpdate { get { return IgnoreColumns; } set { IgnoreColumns = value; } } 12 | public Expression> UpdateOnCondition { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/DatabaseExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Data.Entity; 6 | using System.Linq; 7 | using N.EntityFramework.Extensions.Enums; 8 | using N.EntityFramework.Extensions.Util; 9 | 10 | namespace N.EntityFramework.Extensions 11 | { 12 | public static class DatabaseExtensions 13 | { 14 | public static SqlQuery FromSqlQuery(this Database database, string sqlText, params object[] parameters) 15 | { 16 | var dbConnection = database.Connection; 17 | return new SqlQuery(dbConnection, sqlText, parameters); 18 | } 19 | public static int ClearTable(this Database database, string tableName) 20 | { 21 | var dbConnection = database.Connection; 22 | return SqlUtil.ClearTable(tableName, dbConnection, null); 23 | } 24 | internal static int CloneTable(this Database database, string sourceTable, string destinationTable, IEnumerable columnNames = null, string internalIdColumnName = null) 25 | { 26 | string columns = columnNames != null && columnNames.Count() > 0 ? string.Join(",", CommonUtil.FormatColumns(columnNames)) : "*"; 27 | columns = !string.IsNullOrEmpty(internalIdColumnName) ? string.Format("{0},CAST( NULL AS INT) AS {1}", columns, internalIdColumnName) : columns; 28 | return database.ExecuteSqlCommand(TransactionalBehavior.DoNotEnsureTransaction, string.Format("SELECT TOP 0 {0} INTO {1} FROM {2}", columns, destinationTable, sourceTable)); 29 | } 30 | public static int DropTable(this Database database, string tableName, bool ifExists = false) 31 | { 32 | tableName = CommonUtil.FormatTableName(tableName); 33 | bool deleteTable = !ifExists || (ifExists && database.TableExists(tableName)) ? true : false; 34 | return deleteTable ? database.ExecuteSqlCommand(TransactionalBehavior.DoNotEnsureTransaction, string.Format("DROP TABLE {0}", tableName)) : -1; 35 | } 36 | public static void TruncateTable(this Database database, string tableName, bool ifExists = false) 37 | { 38 | var dbConnection = database.Connection; 39 | bool truncateTable = !ifExists || (ifExists && SqlUtil.TableExists(tableName, dbConnection, null)) ? true : false; 40 | if (truncateTable) 41 | { 42 | database.ExecuteSqlCommand(string.Format("TRUNCATE TABLE {0}", tableName)); 43 | } 44 | } 45 | public static bool TableExists(this Database database, string tableName) 46 | { 47 | return Convert.ToBoolean(database.ExecuteScalar(string.Format("SELECT CASE WHEN OBJECT_ID(N'{0}', N'U') IS NOT NULL THEN 1 ELSE 0 END", tableName))); 48 | } 49 | internal static DbCommand CreateCommand(this Database database, ConnectionBehavior connectionBehavior = ConnectionBehavior.Default) 50 | { 51 | var dbConnection = database.GetConnection(connectionBehavior); 52 | if (dbConnection.State != ConnectionState.Open) 53 | dbConnection.Open(); 54 | return dbConnection.CreateCommand(); 55 | } 56 | internal static object ExecuteScalar(this Database database, string query, object[] parameters = null, int? commandTimeout = null) 57 | { 58 | object value; 59 | var dbConnection = database.Connection; 60 | using (var sqlCommand = dbConnection.CreateCommand()) 61 | { 62 | sqlCommand.CommandText = query; 63 | if (database.CurrentTransaction != null) 64 | sqlCommand.Transaction = database.CurrentTransaction.UnderlyingTransaction; 65 | if (dbConnection.State == ConnectionState.Closed) 66 | dbConnection.Open(); 67 | if (commandTimeout.HasValue) 68 | sqlCommand.CommandTimeout = commandTimeout.Value; 69 | if (parameters != null) 70 | sqlCommand.Parameters.AddRange(parameters); 71 | value = sqlCommand.ExecuteScalar(); 72 | } 73 | return value; 74 | } 75 | internal static DbConnection GetConnection(this Database database, ConnectionBehavior connectionBehavior) 76 | { 77 | return connectionBehavior == ConnectionBehavior.New ? ((ICloneable)database.Connection).Clone() as DbConnection : database.Connection; 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/DatabaseExtensionsAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Entity; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using N.EntityFramework.Extensions.Util; 9 | 10 | namespace N.EntityFramework.Extensions 11 | { 12 | public static class DatabaseExtensionsAsync 13 | { 14 | public async static Task ClearTableAsync(this Database database, string tableName, CancellationToken cancellationToken = default) 15 | { 16 | return await database.ExecuteSqlCommandAsync(string.Format("DELETE FROM {0}", tableName), cancellationToken); 17 | } 18 | internal async static Task CloneTableAsync(this Database database, string sourceTable, string destinationTable, IEnumerable columnNames, string internalIdColumnName = null, CancellationToken cancellationToken = default) 19 | { 20 | string columns = columnNames != null && columnNames.Count() > 0 ? string.Join(",", CommonUtil.FormatColumns(columnNames)) : "*"; 21 | columns = !string.IsNullOrEmpty(internalIdColumnName) ? string.Format("{0},CAST( NULL AS INT) AS {1}", columns, internalIdColumnName) : columns; 22 | return await database.ExecuteSqlCommandAsync(string.Format("SELECT TOP 0 {0} INTO {1} FROM {2}", columns, destinationTable, sourceTable), cancellationToken); 23 | } 24 | public async static Task TruncateTableAsync(this Database database, string tableName, bool ifExists = false, CancellationToken cancellationToken = default) 25 | { 26 | bool truncateTable = !ifExists || (ifExists && database.TableExists(tableName)) ? true : false; 27 | if (truncateTable) 28 | { 29 | await database.ExecuteSqlCommandAsync(string.Format("TRUNCATE TABLE {0}", tableName), cancellationToken); 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/DbTransactionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Data.Entity; 4 | using N.EntityFramework.Extensions.Enums; 5 | 6 | namespace N.EntityFramework.Extensions 7 | { 8 | internal class DbTransactionContext : IDisposable 9 | { 10 | private int? defaultCommandTimeout; 11 | private bool closeConnection; 12 | private bool ownsTransaction; 13 | private DbContext context; 14 | private DbContextTransaction transaction; 15 | private ConnectionBehavior connectionBehavior; 16 | 17 | public DbConnection Connection { get; internal set; } 18 | public DbTransaction CurrentTransaction => transaction != null ? transaction.UnderlyingTransaction: null; 19 | 20 | 21 | public DbTransactionContext(DbContext context, BulkOptions options) : this(context, options.ConnectionBehavior, options.TransactionalBehavior, options.CommandTimeout) 22 | { 23 | 24 | } 25 | public DbTransactionContext(DbContext context, ConnectionBehavior connectionBehavior = ConnectionBehavior.Default, TransactionalBehavior transactionalBehavior = TransactionalBehavior.DoNotEnsureTransaction, int? commandTimeout = null, bool openConnection = true) 26 | { 27 | this.context = context; 28 | this.connectionBehavior = connectionBehavior; 29 | if (connectionBehavior == ConnectionBehavior.Default) 30 | { 31 | this.ownsTransaction = context.Database.CurrentTransaction == null; 32 | this.transaction = context.Database.CurrentTransaction; 33 | } 34 | this.defaultCommandTimeout = context.Database.CommandTimeout; 35 | context.Database.CommandTimeout = commandTimeout; 36 | 37 | if (transaction == null && transactionalBehavior == TransactionalBehavior.EnsureTransaction) 38 | { 39 | this.transaction = context.Database.BeginTransaction(); 40 | } 41 | this.Connection = context.Database.GetConnection(connectionBehavior); 42 | if (openConnection) 43 | { 44 | if (this.Connection.State == System.Data.ConnectionState.Closed) 45 | { 46 | this.Connection.Open(); 47 | this.closeConnection = true; 48 | } 49 | } 50 | } 51 | 52 | public void Dispose() 53 | { 54 | context.Database.CommandTimeout = defaultCommandTimeout; 55 | if (closeConnection | (Connection.State == System.Data.ConnectionState.Open && connectionBehavior == ConnectionBehavior.New)) 56 | { 57 | this.Connection.Close(); 58 | } 59 | } 60 | 61 | internal void Commit() 62 | { 63 | if (this.ownsTransaction && this.transaction != null) 64 | transaction.Commit(); 65 | } 66 | internal void Rollback() 67 | { 68 | if (this.ownsTransaction && transaction != null) 69 | transaction.Rollback(); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/EfExtensionsCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Data.Entity.Infrastructure.Interception; 3 | 4 | namespace N.EntityFramework.Extensions 5 | { 6 | class EfExtensionsCommand 7 | { 8 | public EfExtensionsCommandType CommandType { get; set; } 9 | public string OldValue { get; set; } 10 | public string NewValue { get; set; } 11 | public DbConnection Connection { get; internal set; } 12 | 13 | internal bool Execute(DbCommand command, DbCommandInterceptionContext interceptionContext) 14 | { 15 | if (CommandType == EfExtensionsCommandType.ChangeTableName) 16 | { 17 | command.CommandText = command.CommandText.Replace(OldValue, NewValue); 18 | } 19 | 20 | return true; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/EfExtensionsCommandInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Data.Common; 5 | using System.Data.Entity.Infrastructure.Interception; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace N.EntityFramework.Extensions 11 | { 12 | public class EfExtensionsCommandInterceptor : IDbCommandInterceptor 13 | { 14 | private ConcurrentDictionary extensionCommands = new ConcurrentDictionary(); 15 | public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) 16 | { 17 | 18 | } 19 | 20 | public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) 21 | { 22 | 23 | } 24 | 25 | public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) 26 | { 27 | 28 | } 29 | 30 | public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) 31 | { 32 | foreach (var extensionCommand in extensionCommands) 33 | { 34 | if (extensionCommand.Value.Connection == command.Connection) 35 | { 36 | extensionCommand.Value.Execute(command, interceptionContext); 37 | extensionCommands.TryRemove(extensionCommand.Key, out _); 38 | } 39 | } 40 | } 41 | 42 | public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) 43 | { 44 | 45 | } 46 | 47 | public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) 48 | { 49 | 50 | } 51 | 52 | internal void AddCommand(Guid clientConnectionId, EfExtensionsCommand efExtensionsCommand) 53 | { 54 | extensionCommands.TryAdd(clientConnectionId, efExtensionsCommand); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/EntityDataReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Data.Entity.Core.Mapping; 6 | using System.Dynamic; 7 | using System.Linq; 8 | using System.Linq.Expressions; 9 | using N.EntityFramework.Extensions.Common; 10 | 11 | namespace N.EntityFramework.Extensions 12 | { 13 | internal class EntityDataReader : IDataReader 14 | { 15 | public TableMapping TableMapping { get; set; } 16 | public Dictionary EntityMap { get; set; } 17 | private readonly Dictionary columnNames; 18 | private readonly Dictionary columnIndexes; 19 | private long currentId; 20 | private readonly bool useInternalId; 21 | private readonly int tableFieldCount; 22 | private readonly IEnumerable entities; 23 | private readonly IEnumerator enumerator; 24 | private Dictionary> selectors; 25 | private EntityValueMode valueMode; 26 | private readonly Dictionary conditions; 27 | 28 | public EntityDataReader(TableMapping tableMapping, IEnumerable entities, IEnumerable inputColumns, bool useInternalId) 29 | { 30 | this.columnNames = new Dictionary(); 31 | this.columnIndexes = new Dictionary(); 32 | this.currentId = 0; 33 | this.useInternalId = useInternalId; 34 | this.tableFieldCount = tableMapping.Columns.Count; 35 | this.entities = entities; 36 | this.enumerator = entities.GetEnumerator(); 37 | this.selectors = new Dictionary>(); 38 | this.conditions = new Dictionary(); 39 | this.EntityMap = new Dictionary(); 40 | this.FieldCount = 0; 41 | this.TableMapping = tableMapping; 42 | 43 | int i = 0; 44 | foreach (var column in tableMapping.Columns) 45 | { 46 | if (inputColumns == null || (inputColumns != null && inputColumns.Contains(column.Column.Name))) 47 | { 48 | columnIndexes[column.Property.Name] = i; 49 | columnNames[i] = column.Property.Name; 50 | i++; 51 | } 52 | } 53 | 54 | var type = typeof(T); 55 | if (type.IsValueType || type == typeof(string)) 56 | { 57 | this.valueMode = EntityValueMode.Value; 58 | } 59 | else if (type == typeof(object)) 60 | { 61 | this.valueMode = EntityValueMode.Object; 62 | } 63 | else if (type.IsArray) 64 | { 65 | this.valueMode = EntityValueMode.Array; 66 | } 67 | else 68 | { 69 | this.valueMode = EntityValueMode.MemberAccess; 70 | foreach (var column in columnIndexes) 71 | { 72 | var typeExpression = Expression.Parameter(type, "type"); 73 | var propertyExpression = Expression.PropertyOrField(typeExpression, column.Key); 74 | var expression = Expression.Lambda>(Expression.Convert(propertyExpression, typeof(object)), typeExpression); 75 | selectors[column.Value] = expression.Compile(); 76 | } 77 | } 78 | foreach (var condition in TableMapping.Conditions) 79 | { 80 | conditions[i] = condition; 81 | columnIndexes[condition.Column.Name] = i; 82 | i++; 83 | } 84 | if (useInternalId) 85 | { 86 | columnIndexes[Constants.InternalId_ColumnName] = i; 87 | i++; 88 | } 89 | 90 | this.FieldCount = i; 91 | } 92 | 93 | public object this[int i] => throw new NotImplementedException(); 94 | 95 | public object this[string name] => throw new NotImplementedException(); 96 | 97 | public int Depth { get; set; } 98 | 99 | public bool IsClosed => throw new NotImplementedException(); 100 | 101 | public int RecordsAffected => throw new NotImplementedException(); 102 | 103 | public int FieldCount { get; set; } 104 | 105 | public void Close() 106 | { 107 | throw new NotImplementedException(); 108 | } 109 | 110 | public void Dispose() 111 | { 112 | selectors = null; 113 | enumerator.Dispose(); 114 | } 115 | 116 | public bool GetBoolean(int i) 117 | { 118 | throw new NotImplementedException(); 119 | } 120 | 121 | public byte GetByte(int i) 122 | { 123 | throw new NotImplementedException(); 124 | } 125 | 126 | public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) 127 | { 128 | throw new NotImplementedException(); 129 | } 130 | 131 | public char GetChar(int i) 132 | { 133 | throw new NotImplementedException(); 134 | } 135 | 136 | public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) 137 | { 138 | throw new NotImplementedException(); 139 | } 140 | 141 | public IDataReader GetData(int i) 142 | { 143 | throw new NotImplementedException(); 144 | } 145 | 146 | public string GetDataTypeName(int i) 147 | { 148 | throw new NotImplementedException(); 149 | } 150 | 151 | public DateTime GetDateTime(int i) 152 | { 153 | throw new NotImplementedException(); 154 | } 155 | 156 | public decimal GetDecimal(int i) 157 | { 158 | throw new NotImplementedException(); 159 | } 160 | 161 | public double GetDouble(int i) 162 | { 163 | throw new NotImplementedException(); 164 | } 165 | 166 | public Type GetFieldType(int i) 167 | { 168 | throw new NotImplementedException(); 169 | } 170 | 171 | public float GetFloat(int i) 172 | { 173 | throw new NotImplementedException(); 174 | } 175 | 176 | public Guid GetGuid(int i) 177 | { 178 | throw new NotImplementedException(); 179 | } 180 | 181 | public short GetInt16(int i) 182 | { 183 | throw new NotImplementedException(); 184 | } 185 | 186 | public int GetInt32(int i) 187 | { 188 | throw new NotImplementedException(); 189 | } 190 | 191 | public long GetInt64(int i) 192 | { 193 | throw new NotImplementedException(); 194 | } 195 | 196 | public string GetName(int i) 197 | { 198 | throw new NotImplementedException(); 199 | } 200 | 201 | public int GetOrdinal(string name) 202 | { 203 | return columnIndexes[name]; 204 | } 205 | 206 | public DataTable GetSchemaTable() 207 | { 208 | throw new NotImplementedException(); 209 | } 210 | 211 | public string GetString(int i) 212 | { 213 | throw new NotImplementedException(); 214 | } 215 | 216 | public object GetValue(int i) 217 | { 218 | if (useInternalId && i == this.FieldCount - 1) 219 | { 220 | return this.currentId; 221 | } 222 | else 223 | { 224 | if (this.valueMode == EntityValueMode.Value) 225 | { 226 | return enumerator.Current; 227 | } 228 | else if (this.valueMode == EntityValueMode.Object) 229 | { 230 | var obj = (dynamic)enumerator.Current; 231 | if (obj is IDynamicMetaObjectProvider) 232 | { 233 | return ((IDictionary)obj)[this.columnNames[i]]; 234 | } 235 | else if (i < this.columnNames.Count) 236 | { 237 | var property = obj.GetType().GetProperty(this.columnNames[i]); 238 | if (property != null) 239 | { 240 | return property.GetValue(obj, null); 241 | } 242 | else 243 | { 244 | return obj; 245 | } 246 | } 247 | else 248 | { 249 | return conditions[i].GetPrivateFieldValue("Value"); 250 | } 251 | } 252 | else if (this.valueMode == EntityValueMode.Array) 253 | { 254 | var array = enumerator.Current as object[]; 255 | return array[i]; 256 | } 257 | else 258 | { 259 | return i < selectors.Count ? selectors[i](enumerator.Current) : conditions[i].GetPrivateFieldValue("Value"); 260 | } 261 | } 262 | 263 | } 264 | 265 | public int GetValues(object[] values) 266 | { 267 | throw new NotImplementedException(); 268 | } 269 | 270 | public bool IsDBNull(int i) 271 | { 272 | throw new NotImplementedException(); 273 | } 274 | 275 | public bool NextResult() 276 | { 277 | throw new NotImplementedException(); 278 | } 279 | 280 | public bool Read() 281 | { 282 | bool moveNext = enumerator.MoveNext(); 283 | 284 | if (moveNext && this.useInternalId) 285 | { 286 | this.currentId++; 287 | this.EntityMap.Add(this.currentId, enumerator.Current); 288 | } 289 | return moveNext; 290 | } 291 | } 292 | 293 | public enum EntityValueMode 294 | { 295 | Value = 1, 296 | Object = 2, 297 | MemberAccess = 3, 298 | Array = 4 299 | } 300 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/FetchOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace N.EntityFramework.Extensions 9 | { 10 | public class FetchOptions 11 | { 12 | public Expression> IgnoreColumns { get; set; } 13 | public Expression> InputColumns { get; set; } 14 | public int BatchSize { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/FetchResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace N.EntityFramework.Extensions 5 | { 6 | public class FetchResult 7 | { 8 | public List Results { get; set; } 9 | public int Batch { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/QueryToFileOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions 8 | { 9 | public class QueryToFileOptions 10 | { 11 | public string ColumnDelimiter { get; set; } 12 | public int? CommandTimeout { get; set; } 13 | public bool IncludeHeaderRow { get; set; } 14 | public string RowDelimiter { get; set; } 15 | public string TextQualifer { get; set; } 16 | 17 | public QueryToFileOptions() 18 | { 19 | ColumnDelimiter = ","; 20 | IncludeHeaderRow = true; 21 | RowDelimiter = "\r\n"; 22 | TextQualifer = ""; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/QueryToFileResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace N.EntityFramework.Extensions 4 | { 5 | public class QueryToFileResult 6 | { 7 | public long BytesWritten { get; set; } 8 | public int DataRowCount { get; internal set; } 9 | public int TotalRowCount { get; internal set; } 10 | } 11 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/SqlMergeAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions 8 | { 9 | internal static class SqlMergeAction 10 | { 11 | public static string Insert = "INSERT"; 12 | public static string Update = "UPDATE"; 13 | public static string Delete = "DELETE"; 14 | } 15 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/SqlQuery.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Threading.Tasks; 4 | using N.EntityFramework.Extensions.Sql; 5 | 6 | namespace N.EntityFramework.Extensions 7 | { 8 | public class SqlQuery 9 | { 10 | private DbConnection Connection { get; set; } 11 | public string SqlText { get; private set; } 12 | public object[] Parameters { get; private set; } 13 | 14 | public SqlQuery(DbConnection sqlConnection, String sqlText, params object[] parameters) 15 | { 16 | this.Connection = sqlConnection; 17 | this.SqlText = sqlText; 18 | this.Parameters = parameters; 19 | } 20 | public int Count() 21 | { 22 | string countSqlText = SqlBuilder.Parse(this.SqlText).Count(); 23 | return (int)SqlUtil.ExecuteScalar(countSqlText, this.Connection, null, this.Parameters); 24 | } 25 | public async Task CountAsync() 26 | { 27 | string countSqlText = SqlBuilder.Parse(this.SqlText).Count(); 28 | return (int)(await SqlUtil.ExecuteScalarAsync(countSqlText, this.Connection, null, this.Parameters)); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Data/TableMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Entity; 4 | using System.Data.Entity.Core.Mapping; 5 | using System.Data.Entity.Core.Metadata.Edm; 6 | using System.Linq; 7 | 8 | namespace N.EntityFramework.Extensions 9 | { 10 | public class TableMapping 11 | { 12 | public EntitySetMapping Mapping { get; set; } 13 | public EntitySet EntitySet { get; set; } 14 | public EntityType EntityType { get; set; } 15 | public Type ClrType { get; set; } 16 | public IEnumerable EntityTypes { get; } 17 | public List Columns { get; set; } 18 | public List Conditions { get; set; } 19 | public string Schema { get; } 20 | public string TableName { get; } 21 | public string FullQualifedTableName 22 | { 23 | get { return string.Format("[{0}].[{1}]", this.Schema, this.TableName); } 24 | } 25 | 26 | public bool HasIdentity => this.Columns.Any(o => o.Column.IsStoreGeneratedIdentity); 27 | 28 | public TableMapping(EntitySet entitySet, EntityType entityType, Type clrType, EntitySetMapping mapping, 29 | List columns, List conditions) 30 | { 31 | var storeEntitySet = mapping.EntityTypeMappings.First(o => o.EntityType != null && o.EntityType.Name == entityType.Name).Fragments.Single().StoreEntitySet; 32 | 33 | ClrType = clrType; 34 | EntitySet = entitySet; 35 | EntityType = entityType; 36 | EntityTypes = GetEntityTypes().Reverse(); 37 | Mapping = mapping; 38 | Columns = columns; 39 | Conditions = conditions; 40 | Schema = (string)storeEntitySet.MetadataProperties["Schema"].Value ?? storeEntitySet.Schema; 41 | TableName = (string)storeEntitySet.MetadataProperties["Table"].Value ?? storeEntitySet.Name; 42 | } 43 | 44 | public IEnumerable GetAutoGeneratedColumns(bool storeGeneratedIdentity = true) 45 | { 46 | return this.Columns.Where(o => o.Column.IsStoreGeneratedComputed || (storeGeneratedIdentity && o.Column.IsStoreGeneratedIdentity)).Select(o => o.Column.Name); 47 | } 48 | public IEnumerable GetColumns(bool includeIdentity = false, bool includeComputed = false) 49 | { 50 | var columns = new List(); 51 | columns.AddRange(this.Columns.Where(o => (!o.Column.IsStoreGeneratedComputed || includeComputed) && (includeIdentity || !o.Column.IsStoreGeneratedIdentity)).Select(o => o.Column.Name)); 52 | columns.AddRange(this.Conditions.Select(o => o.Column.Name)); 53 | return columns; 54 | } 55 | public IEnumerable GetQualifiedColumnNames(IEnumerable columnNames) 56 | { 57 | string format = "[{0}].[{1}]"; 58 | var columns = new List(); 59 | columns.AddRange(this.Columns.Where(o => columnNames.Contains(o.Column.Name)) 60 | .Select(o => string.Format(format, FindTableName(o.Column.DeclaringType as EntityType), o.Column.Name))); 61 | columns.AddRange(this.Conditions.Where(o => columnNames.Contains(o.Column.Name)) 62 | .Select(o => string.Format(format, FindTableName(o.Column.DeclaringType as EntityType), o.Column.Name))); 63 | return columns; 64 | } 65 | public IEnumerable GetPrimaryKeyColumns() => 66 | EntitySet.ElementType.KeyMembers.Select(o => Columns.Single(c => c.Property.Name == o.Name).Column.Name); 67 | public IEnumerable GetTableNames() 68 | { 69 | foreach (var entityType in EntityTypes) 70 | { 71 | var storeEntitySet = Mapping.EntityTypeMappings.First(t => (t.EntityType != null && t.EntityType.Name == entityType.Name) 72 | || t.IsOfEntityTypes.Any(o => o.Name == entityType.Name)).Fragments.Single().StoreEntitySet; 73 | yield return FindTableName(storeEntitySet); 74 | } 75 | } 76 | private string FindTableName(EntitySet storeEntitySet) 77 | { 78 | return (string)storeEntitySet.MetadataProperties["Table"].Value ?? storeEntitySet.Name; 79 | } 80 | private string FindTableName(EntityType entityType) 81 | { 82 | var storeEntitySet = Mapping.EntityTypeMappings.First(t => (t.EntityType != null && t.EntityType.Name == entityType.Name) 83 | || t.IsOfEntityTypes.Any(o => o.Name == entityType.Name)).Fragments.Single().StoreEntitySet; 84 | return FindTableName(storeEntitySet); 85 | } 86 | private IEnumerable GetEntityTypes() 87 | { 88 | EntityType entityType = EntityType; 89 | do 90 | { 91 | yield return entityType; 92 | entityType = entityType.BaseType as EntityType; 93 | } while (entityType != null); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Enums/ConnectionBehavior.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions.Enums 8 | { 9 | public enum ConnectionBehavior 10 | { 11 | Default, 12 | New 13 | } 14 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Enums/EfExtensionsCommandType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions 8 | { 9 | enum EfExtensionsCommandType 10 | { 11 | ChangeTableName 12 | } 13 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Extensions/CommonExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions.Extensions 8 | { 9 | internal static class CommonExtensions 10 | { 11 | internal static T Build(this Action buildAction) where T : new() 12 | { 13 | var parameter = new T(); 14 | buildAction(parameter); 15 | return parameter; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Extensions/LinqExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace N.EntityFramework.Extensions 11 | { 12 | static class LinqExtensions 13 | { 14 | internal static string GetExpressionValueAsString(MemberBinding binding) 15 | { 16 | return GetExpressionValueAsString(binding.GetPrivateFieldValue("Expression") as Expression); 17 | } 18 | internal static string GetExpressionValueAsString(Expression expression) 19 | { 20 | if (expression.NodeType == ExpressionType.Constant) 21 | { 22 | return ConvertToSqlValue((expression as ConstantExpression).Value); 23 | } 24 | else if (expression.NodeType == ExpressionType.MemberAccess) 25 | { 26 | if (expression.GetPrivateFieldValue("Expression") is ParameterExpression parameterExpression) 27 | { 28 | return Expression.Lambda(expression).Body.ToString(); 29 | } 30 | else 31 | { 32 | return ConvertToSqlValue(Expression.Lambda(expression).Compile().DynamicInvoke()); 33 | } 34 | } 35 | else if (expression.NodeType == ExpressionType.Convert) 36 | { 37 | return ConvertToSqlValue(Expression.Lambda(expression).Compile().DynamicInvoke()); 38 | } 39 | else if (expression.NodeType == ExpressionType.Call) 40 | { 41 | var methodCallExpression = expression as MethodCallExpression; 42 | List argValues = new List(); 43 | foreach (var argument in methodCallExpression.Arguments) 44 | { 45 | argValues.Add(GetExpressionValueAsString(argument)); 46 | } 47 | string methodFormat; 48 | switch (methodCallExpression.Method.Name) 49 | { 50 | case "ToString": 51 | methodFormat = string.Format("CONVERT(VARCHAR,{0})", argValues[0]); 52 | break; 53 | default: 54 | methodFormat = string.Format("{0}({1})", methodCallExpression.Method.Name, string.Join(",", argValues)); 55 | break; 56 | } 57 | return methodFormat; 58 | } 59 | else 60 | { 61 | var leftExpression = expression.GetPrivateFieldValue("Left") as Expression; 62 | var rightExpression = expression.GetPrivateFieldValue("Right") as Expression; 63 | string leftValue = GetExpressionValueAsString(leftExpression); 64 | string rightValue = GetExpressionValueAsString(rightExpression); 65 | string joinValue = string.Empty; 66 | switch (expression.NodeType) 67 | { 68 | case ExpressionType.Add: 69 | joinValue = "+"; 70 | break; 71 | case ExpressionType.Subtract: 72 | joinValue = "-"; 73 | break; 74 | case ExpressionType.Multiply: 75 | joinValue = "*"; 76 | break; 77 | case ExpressionType.Divide: 78 | joinValue = "/"; 79 | break; 80 | case ExpressionType.Modulo: 81 | joinValue = "%"; 82 | break; 83 | } 84 | return string.Format("({0} {1} {2})", leftValue, joinValue, rightValue); 85 | } 86 | } 87 | 88 | private static string ConvertToSqlValue(object value) 89 | { 90 | if (value == null) 91 | return "NULL"; 92 | if (value is string str) 93 | return "'" + str.Replace("'", "''") + "'"; 94 | if (value is Guid guid) 95 | return $"'{guid}'"; 96 | if (value is bool b) 97 | return b ? "1" : "0"; 98 | if (value is DateTime dt) 99 | return "CAST('" + dt.ToString("yyyy-MM-ddTHH:mm:ss.fffffff") + "' AS DATETIME2)"; // Convert to ISO-8601 100 | if (value is DateTimeOffset dto) 101 | return "'" + dto.ToString("yyyy-MM-ddTHH:mm:ss.fffffffzzzz") + "'"; // Convert to ISO-8601 102 | var valueType = value.GetType(); 103 | if (valueType.IsEnum) 104 | return Convert.ToString((int)value); 105 | if (!valueType.IsClass) 106 | return Convert.ToString(value, CultureInfo.InvariantCulture); 107 | 108 | throw new NotImplementedException("Unhandled data type."); 109 | } 110 | public static List GetObjectProperties(this Expression> expression) 111 | { 112 | if (expression == null) 113 | { 114 | return new List(); 115 | } 116 | else if (expression.Body is MemberExpression propertyExpression) 117 | { 118 | return new List() { propertyExpression.Member.Name }; 119 | } 120 | else if (expression.Body is NewExpression newExpression) 121 | { 122 | return newExpression.Members.Select(o => o.Name).ToList(); 123 | } 124 | else if ((expression.Body is UnaryExpression unaryExpression) && (unaryExpression.Operand.GetPrivateFieldValue("Member") is PropertyInfo propertyInfo)) 125 | { 126 | return new List() { propertyInfo.Name }; 127 | } 128 | else 129 | { 130 | throw new InvalidOperationException("GetObjectProperties() encountered an unsupported expression type"); 131 | } 132 | } 133 | internal static string ToSqlPredicate(this Expression expression, params string[] parameters) 134 | { 135 | var stringBuilder = new StringBuilder((string)expression.Body.GetPrivateFieldValue("DebugView")); 136 | int i = 0; 137 | foreach (var expressionParam in expression.Parameters) 138 | { 139 | if (parameters.Length <= i) break; 140 | stringBuilder.Replace((string)expressionParam.GetPrivateFieldValue("DebugView"), parameters[i]); 141 | i++; 142 | } 143 | stringBuilder.Replace("&&", "AND"); 144 | stringBuilder.Replace("==", "="); 145 | return stringBuilder.ToString(); 146 | } 147 | internal static string ToSqlUpdateSetExpression(this Expression expression, string tableName) 148 | { 149 | List setValues = new List(); 150 | var memberInitExpression = expression.Body as MemberInitExpression; 151 | foreach (var binding in memberInitExpression.Bindings) 152 | { 153 | string expValue = GetExpressionValueAsString(binding); 154 | expValue = expValue.Replace(string.Format("{0}.", expression.Parameters.First().Name), 155 | string.Format("{0}.", tableName)); 156 | setValues.Add(string.Format("{0}.[{1}]={2}", tableName, binding.Member.Name, expValue)); 157 | } 158 | return string.Join(",", setValues); 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Extensions/ObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace N.EntityFramework.Extensions 6 | { 7 | internal static class ObjectExtensions 8 | { 9 | public static object GetPrivateFieldValue(this object obj, string propName) 10 | { 11 | if (obj == null) throw new ArgumentNullException("obj"); 12 | Type t = obj.GetType(); 13 | FieldInfo fieldInfo = null; 14 | PropertyInfo propertyInfo = null; 15 | while (fieldInfo == null && propertyInfo == null && t != null) 16 | { 17 | fieldInfo = t.GetField(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 18 | if (fieldInfo == null) 19 | { 20 | propertyInfo = t.GetProperty(propName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 21 | } 22 | 23 | t = t.BaseType; 24 | } 25 | if (fieldInfo == null && propertyInfo == null) 26 | throw new ArgumentOutOfRangeException("propName", string.Format("Field {0} was not found in Type {1}", propName, obj.GetType().FullName)); 27 | 28 | if (fieldInfo != null) 29 | return fieldInfo.GetValue(obj); 30 | 31 | return propertyInfo.GetValue(obj, null); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/N.EntityFramework.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net462;netstandard2.1 4 | Library 5 | 1.9.2 6 | NorthernLight1 7 | Copyright © 2024 8 | 9 | Entity Framework Extensions extends your DbContext with high-performance bulk operations: BulkDelete, BulkFetch, BulkInsert, BulkMerge, BulkSaveChanges, BulkSync, BulkUpdate, Fetch, FromSqlQuery, DeleteFromQuery, InsertFromQuery, UpdateFromQuery, QueryToCsvFile, SqlQueryToCsvFile. 10 | 11 | Supports: Transaction, Asynchronous Execution, Inheritance Models (Table-Per-Hierarchy, Table-Per-Concrete) 12 | 13 | https://github.com/NorthernLight1/N.EntityFramework.Extensions/ 14 | NorthernLight1 15 | EntityFramework Extensions 16 | true 17 | MIT 18 | README.md 19 | 20 | 21 | 22 | True 23 | \ 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Sql/SqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Data.Entity.Core.Objects; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | 9 | namespace N.EntityFramework.Extensions.Sql 10 | { 11 | internal class SqlBuilder 12 | { 13 | private static IEnumerable keywords = new string[] { "SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY" }; 14 | public string Sql 15 | { 16 | get { return this.ToString(); } 17 | } 18 | public DbParameter[] Parameters { get; private set; } 19 | public List Clauses { get; private set; } 20 | private SqlBuilder(string sql, DbParameter[] parameters = default) 21 | { 22 | Clauses = new List(); 23 | Parameters = parameters == null ? new DbParameter[0] : parameters; 24 | Initialize(sql); 25 | } 26 | 27 | private void Initialize(string sqlText) 28 | { 29 | //Clean Sql Text 30 | sqlText = sqlText.Replace("\r\n", " "); 31 | //Process Sql Text 32 | string curClause = string.Empty; 33 | int curClauseIndex = 0, wrappedCount = 0; 34 | for (int i = 0; i < sqlText.Length;) 35 | { 36 | //Find new Sql clause 37 | int maxLenToSearch = sqlText.Length - i >= 10 ? 10 : sqlText.Length - i; 38 | string keyword = StartsWithString(sqlText.Substring(i, maxLenToSearch), keywords, StringComparison.OrdinalIgnoreCase); 39 | bool isWordStart = i > 0 ? sqlText[i - 1] == ' ' : true; 40 | 41 | if (sqlText[i] == '(') 42 | wrappedCount++; 43 | else if (sqlText[i] == ')') 44 | wrappedCount--; 45 | 46 | //Process Sql clause 47 | if (keyword != null && curClause != keyword && isWordStart && wrappedCount == 0) 48 | { 49 | if (!string.IsNullOrEmpty(curClause)) 50 | { 51 | Clauses.Add(SqlClause.Parse(curClause, sqlText.Substring(curClauseIndex, i - curClauseIndex))); 52 | } 53 | curClause = keyword; 54 | curClauseIndex = i + curClause.Length; 55 | i = i + curClause.Length; 56 | } 57 | else 58 | { 59 | i++; 60 | } 61 | } 62 | if (!string.IsNullOrEmpty(curClause)) 63 | Clauses.Add(SqlClause.Parse(curClause, sqlText.Substring(curClauseIndex))); 64 | } 65 | public string Count() 66 | { 67 | return string.Format("SELECT COUNT(*) FROM ({0}) s", string.Join("\r\n", Clauses.Where(o => o.Name != "ORDER BY").Select(o => o.ToString()))); 68 | } 69 | public String GetTableAlias() 70 | { 71 | var sqlFromClause = Clauses.First(o => o.Name == "FROM"); 72 | var startIndex = sqlFromClause.InputText.LastIndexOf(" AS "); 73 | return startIndex > 0 ? sqlFromClause.InputText.Substring(startIndex + 4) : ""; 74 | } 75 | public override string ToString() 76 | { 77 | return string.Join("\r\n", Clauses.Select(o => o.ToString())); 78 | } 79 | private static string StartsWithString(string textToSearch, IEnumerable valuesToFind, StringComparison stringComparison) 80 | { 81 | string value = null; 82 | foreach (var valueToFind in valuesToFind) 83 | { 84 | bool isWord = textToSearch.Length > valueToFind.Length && textToSearch[valueToFind.Length] == ' '; 85 | if (textToSearch.StartsWith(valueToFind, stringComparison) && isWord) 86 | { 87 | value = valueToFind; 88 | break; 89 | } 90 | } 91 | 92 | return value; 93 | } 94 | public static SqlBuilder Parse(string sql) 95 | { 96 | return new SqlBuilder(sql); 97 | } 98 | public static SqlBuilder Parse(string sql, ObjectQuery objectQuery) 99 | { 100 | var sqlParameters = new List(); 101 | 102 | if (objectQuery != null) 103 | { 104 | foreach (var parameter in objectQuery.Parameters) 105 | { 106 | DbParameter sqlParameter; 107 | 108 | if (objectQuery.Context.Connection is System.Data.SqlClient.SqlConnection || 109 | objectQuery.Context.Connection is System.Data.Entity.Core.EntityClient.EntityConnection) 110 | { 111 | sqlParameter = new System.Data.SqlClient.SqlParameter(parameter.Name, parameter.Value); 112 | } 113 | else if (objectQuery.Context.Connection is Microsoft.Data.SqlClient.SqlConnection) 114 | { 115 | sqlParameter = new Microsoft.Data.SqlClient.SqlParameter(parameter.Name, parameter.Value); 116 | } 117 | else 118 | { 119 | throw new NotSupportedException("Unsupported provider: " + objectQuery.Context.Connection.GetType().Namespace); 120 | } 121 | 122 | if (sqlParameter.DbType == System.Data.DbType.DateTime) 123 | { 124 | sqlParameter.DbType = System.Data.DbType.DateTime2; 125 | } 126 | 127 | sqlParameters.Add(sqlParameter); 128 | } 129 | } 130 | 131 | return new SqlBuilder(sql, sqlParameters.ToArray()); 132 | } 133 | public void ChangeToDelete() 134 | { 135 | Validate(); 136 | var sqlClause = Clauses.FirstOrDefault(); 137 | var sqlFromClause = Clauses.First(o => o.Name == "FROM"); 138 | if (sqlClause != null) 139 | { 140 | sqlClause.Name = "DELETE"; 141 | int searchStartIndex = sqlFromClause.InputText.LastIndexOf(")"); 142 | searchStartIndex = searchStartIndex == -1 ? 0 : searchStartIndex; 143 | int aliasStartIndex = sqlFromClause.InputText.IndexOf("AS ", searchStartIndex) + 3; 144 | int aliasLength = sqlFromClause.InputText.IndexOf("]", aliasStartIndex) - aliasStartIndex + 1; 145 | sqlClause.InputText = sqlFromClause.InputText.Substring(aliasStartIndex, aliasLength); 146 | } 147 | } 148 | public void ChangeToUpdate(string tableName, Expression> updateExpression) 149 | { 150 | Validate(); 151 | string setSqlExpression = updateExpression.ToSqlUpdateSetExpression(tableName); 152 | var sqlClause = Clauses.FirstOrDefault(); 153 | if (sqlClause != null) 154 | { 155 | sqlClause.Name = "UPDATE"; 156 | sqlClause.InputText = tableName; 157 | Clauses.Insert(1, new SqlClause { Name = "SET", InputText = setSqlExpression }); 158 | } 159 | } 160 | internal void ChangeToInsert(string tableName, Expression> insertObjectExpression) 161 | { 162 | Validate(); 163 | var sqlSelectClause = Clauses.FirstOrDefault(); 164 | string columnsToInsert = string.Join(",", insertObjectExpression.GetObjectProperties()); 165 | string insertValueExpression = string.Format("INTO {0} ({1})", tableName, columnsToInsert); 166 | Clauses.Insert(0, new SqlClause { Name = "INSERT", InputText = insertValueExpression }); 167 | sqlSelectClause.InputText = columnsToInsert; 168 | } 169 | internal void SelectColumns(IEnumerable columns) 170 | { 171 | var tableAlias = GetTableAlias(); 172 | var sqlClause = Clauses.FirstOrDefault(); 173 | if (sqlClause.Name == "SELECT") 174 | { 175 | sqlClause.InputText = string.Join(",", columns.Select(c => string.Format("{0}.{1}", tableAlias, c))); 176 | } 177 | } 178 | private void Validate() 179 | { 180 | if (Clauses.Count == 0) 181 | { 182 | throw new Exception("You must parse a valid sql statement before you can use this function."); 183 | } 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Sql/SqlClause.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions.Sql 8 | { 9 | class SqlClause 10 | { 11 | internal string Name { get; set; } 12 | internal string InputText { get; set; } 13 | public string Sql 14 | { 15 | get { return this.ToString(); } 16 | } 17 | public static SqlClause Parse(string name, string inputText) 18 | { 19 | //string cleanText = inputText.Replace("\r\n", " ").Trim(); 20 | return new SqlClause { Name = name, InputText = inputText.Trim() }; 21 | } 22 | public override string ToString() 23 | { 24 | return string.Format("{0} {1}", Name, InputText); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Util/CommonUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Common; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | 8 | namespace N.EntityFramework.Extensions.Util 9 | { 10 | internal static class CommonUtil 11 | { 12 | internal static string GetStagingTableName(TableMapping tableMapping, bool usePermanentTable, DbConnection dbConnection) 13 | { 14 | string tableName = string.Empty; 15 | if (usePermanentTable) 16 | { 17 | string clientConnectionId = SqlClientUtil.GetClientConnectionId(dbConnection); 18 | 19 | tableName = string.Format("[{0}].[tmp_be_xx_{1}_{2}]", tableMapping.Schema, tableMapping.TableName, clientConnectionId); 20 | } 21 | else 22 | tableName = string.Format("[{0}].[#tmp_be_xx_{1}]", tableMapping.Schema, tableMapping.TableName); 23 | return tableName; 24 | } 25 | internal static IEnumerable FormatColumns(IEnumerable columns) 26 | { 27 | return columns.Select(s => s.StartsWith("[") && s.EndsWith("]") ? s : string.Format("[{0}]", s)); 28 | } 29 | internal static IEnumerable FormatColumns(string tableAlias, IEnumerable columns) 30 | { 31 | return columns.Select(s => s.StartsWith("[") && s.EndsWith("]") ? string.Format("[{0}].{1}", tableAlias, s) : string.Format("[{0}].[{1}]", tableAlias, s)); 32 | } 33 | internal static IEnumerable FilterColumns(IEnumerable columnNames, string[] primaryKeyColumnNames, Expression> inputColumns = null, Expression> ignoreColumns = null) 34 | { 35 | var filteredColumnNames = columnNames; 36 | if (inputColumns != null) 37 | { 38 | var inputColumnNames = inputColumns.GetObjectProperties(); 39 | filteredColumnNames = filteredColumnNames.Intersect(inputColumnNames); 40 | } 41 | if (ignoreColumns != null) 42 | { 43 | var ignoreColumnNames = ignoreColumns.GetObjectProperties(); 44 | if (ignoreColumnNames.Intersect(primaryKeyColumnNames).Any()) 45 | { 46 | throw new InvalidDataException("Primary key columns can not be ignored in BulkInsertOptions.IgnoreColumns"); 47 | } 48 | else 49 | { 50 | filteredColumnNames = filteredColumnNames.Except(ignoreColumnNames); 51 | } 52 | } 53 | return filteredColumnNames; 54 | } 55 | internal static string FormatTableName(string tableName) 56 | { 57 | return string.Join(".", tableName.Split('.').Select(s => $"[{RemoveQualifier(s)}]")); 58 | } 59 | private static string RemoveQualifier(string name) 60 | { 61 | return name.TrimStart('[').TrimEnd(']'); 62 | } 63 | } 64 | internal static class CommonUtil 65 | { 66 | internal static string[] GetColumns(Expression> expression, string[] tableNames) 67 | { 68 | List foundColumns = new List(); 69 | string sqlText = (string)expression.Body.GetPrivateFieldValue("DebugView"); 70 | 71 | int startIndex = sqlText.IndexOf("$"); 72 | while (startIndex != -1) 73 | { 74 | int endIndex = sqlText.IndexOf(" ", startIndex); 75 | string column = endIndex == -1 ? sqlText.Substring(startIndex) : sqlText.Substring(startIndex, endIndex - startIndex); 76 | string[] columnParts = column.Split('.'); 77 | if (tableNames == null || tableNames.Contains(columnParts[0].Remove(0, 1))) 78 | { 79 | foundColumns.Add(columnParts[1]); 80 | } 81 | startIndex = sqlText.IndexOf("$", startIndex + 1); 82 | } 83 | 84 | return foundColumns.ToArray(); 85 | } 86 | internal static string GetJoinConditionSql(Expression> joinKeyExpression, string[] storeGeneratedColumnNames, string sourceTableName = "s", string targetTableName = "t") 87 | { 88 | string joinConditionSql = string.Empty; 89 | if (joinKeyExpression != null) 90 | { 91 | joinConditionSql = joinKeyExpression.ToSqlPredicate(sourceTableName, targetTableName); 92 | } 93 | else 94 | { 95 | int i = 1; 96 | foreach (var storeGeneratedColumnName in storeGeneratedColumnNames) 97 | { 98 | joinConditionSql += (i > 1 ? " AND " : "") + string.Format("{0}.{2}={1}.{2}", sourceTableName, targetTableName, storeGeneratedColumnName); 99 | i++; 100 | } 101 | } 102 | return joinConditionSql; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Util/SqlClientUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Common; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace N.EntityFramework.Extensions.Util 9 | { 10 | internal static class SqlClientUtil 11 | { 12 | 13 | 14 | internal static Enum GetSqlBulkCopyOptionsDefault(DbConnection dbConnection) 15 | { 16 | if (dbConnection is System.Data.SqlClient.SqlConnection) 17 | { 18 | return (Enum)Enum.Parse(typeof(System.Data.SqlClient.SqlBulkCopyOptions), "Default"); 19 | } 20 | else if (dbConnection is Microsoft.Data.SqlClient.SqlConnection) 21 | { 22 | return (Enum)Enum.Parse(typeof(Microsoft.Data.SqlClient.SqlBulkCopyOptions), "Default"); 23 | } 24 | else 25 | { 26 | throw new NotSupportedException("Unsupported provider: " + dbConnection.GetType().Namespace); 27 | } 28 | } 29 | 30 | internal static Enum GetSqlBulkCopyOptionsKeepIdentity(DbConnection dbConnection) 31 | { 32 | if (dbConnection is System.Data.SqlClient.SqlConnection) 33 | { 34 | return (Enum)Enum.Parse(typeof(System.Data.SqlClient.SqlBulkCopyOptions), "KeepIdentity"); 35 | } 36 | else if (dbConnection is Microsoft.Data.SqlClient.SqlConnection) 37 | { 38 | return (Enum)Enum.Parse(typeof(Microsoft.Data.SqlClient.SqlBulkCopyOptions), "KeepIdentity"); 39 | } 40 | else 41 | { 42 | throw new NotSupportedException("Unsupported provider: " + dbConnection.GetType().Namespace); 43 | } 44 | } 45 | internal static dynamic CreateSqlBulkCopy( 46 | DbConnection dbConnection, 47 | DbTransaction transaction, 48 | string tableName, 49 | Enum bulkCopyOptions, 50 | int batchSize, 51 | int? commandTimeout) 52 | { 53 | dynamic sqlBulkCopy; 54 | 55 | if (dbConnection is System.Data.SqlClient.SqlConnection) 56 | { 57 | sqlBulkCopy = new System.Data.SqlClient.SqlBulkCopy((System.Data.SqlClient.SqlConnection)dbConnection, (System.Data.SqlClient.SqlBulkCopyOptions)bulkCopyOptions, (System.Data.SqlClient.SqlTransaction)transaction) 58 | { 59 | DestinationTableName = tableName, 60 | BatchSize = batchSize, 61 | BulkCopyTimeout = commandTimeout ?? 0 62 | }; 63 | } 64 | else if (dbConnection is Microsoft.Data.SqlClient.SqlConnection) 65 | { 66 | sqlBulkCopy = new Microsoft.Data.SqlClient.SqlBulkCopy((Microsoft.Data.SqlClient.SqlConnection)dbConnection, (Microsoft.Data.SqlClient.SqlBulkCopyOptions)bulkCopyOptions, (Microsoft.Data.SqlClient.SqlTransaction)transaction) 67 | { 68 | DestinationTableName = tableName, 69 | BatchSize = batchSize, 70 | BulkCopyTimeout = commandTimeout ?? 0 71 | }; 72 | } 73 | else 74 | { 75 | throw new NotSupportedException("Unsupported provider: " + dbConnection.GetType().Namespace); 76 | } 77 | 78 | return sqlBulkCopy; 79 | } 80 | 81 | internal static string GetClientConnectionId(DbConnection dbConnection) 82 | { 83 | if (dbConnection is System.Data.SqlClient.SqlConnection sqlConnection) 84 | { 85 | return sqlConnection.ClientConnectionId.ToString(); 86 | } 87 | else if (dbConnection is Microsoft.Data.SqlClient.SqlConnection msSqlConnection) 88 | { 89 | return msSqlConnection.ClientConnectionId.ToString(); 90 | } 91 | else 92 | { 93 | throw new NotSupportedException("Unsupported provider: " + dbConnection.GetType().Namespace); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /N.EntityFramework.Extensions/Util/SqlUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Threading.Tasks; 6 | 7 | namespace N.EntityFramework.Extensions 8 | { 9 | internal static class SqlUtil 10 | { 11 | internal static int ExecuteSql(string query, DbConnection connection, DbTransaction transaction, int? commandTimeout = null) 12 | { 13 | return SqlUtil.ExecuteSql(query, connection, transaction, null, commandTimeout); 14 | } 15 | internal static int ExecuteSql(string query, DbConnection connection, DbTransaction transaction, object[] parameters = null, int? commandTimeout = null) 16 | { 17 | using (var dbCommand = connection.CreateCommand()) 18 | { 19 | dbCommand.CommandText = query; 20 | if (transaction != null) 21 | dbCommand.Transaction = transaction; 22 | if (connection.State == ConnectionState.Closed) 23 | connection.Open(); 24 | if (commandTimeout.HasValue) 25 | dbCommand.CommandTimeout = commandTimeout.Value; 26 | if (parameters != null) 27 | dbCommand.Parameters.AddRange(parameters); 28 | return dbCommand.ExecuteNonQuery(); 29 | } 30 | } 31 | internal static object ExecuteScalar(string query, DbConnection connection, DbTransaction transaction, object[] parameters = null, int? commandTimeout = null) 32 | { 33 | 34 | using (var dbCommand = connection.CreateCommand()) 35 | { 36 | dbCommand.CommandText = query; 37 | if (transaction != null) 38 | dbCommand.Transaction = transaction; 39 | if (connection.State == ConnectionState.Closed) 40 | connection.Open(); 41 | if (commandTimeout.HasValue) 42 | dbCommand.CommandTimeout = commandTimeout.Value; 43 | if (parameters != null) 44 | dbCommand.Parameters.AddRange(parameters); 45 | 46 | return dbCommand.ExecuteScalar(); 47 | } 48 | 49 | } 50 | internal static async Task ExecuteScalarAsync(string query, DbConnection connection, DbTransaction transaction, object[] parameters = null, int? commandTimeout = null) 51 | { 52 | using (var dbCommand = connection.CreateCommand()) 53 | { 54 | dbCommand.CommandText = query; 55 | if (transaction != null) 56 | dbCommand.Transaction = transaction; 57 | if (connection.State == ConnectionState.Closed) 58 | await connection.OpenAsync(); 59 | if (commandTimeout.HasValue) 60 | dbCommand.CommandTimeout = commandTimeout.Value; 61 | if (parameters != null) 62 | dbCommand.Parameters.AddRange(parameters); 63 | 64 | return await dbCommand.ExecuteScalarAsync(); 65 | } 66 | } 67 | internal static int ClearTable(string tableName, DbConnection connection, DbTransaction transaction) 68 | { 69 | return ExecuteSql(string.Format("DELETE FROM {0}", tableName), connection, transaction, null); 70 | } 71 | internal static string ConvertToColumnString(IEnumerable columnNames) 72 | { 73 | return string.Join(",", columnNames); 74 | } 75 | internal static int ToggleIdentityInsert(bool enable, string tableName, DbConnection dbConnection, DbTransaction dbTransaction) 76 | { 77 | string boolString = enable ? "ON" : "OFF"; 78 | return ExecuteSql(string.Format("SET IDENTITY_INSERT {0} {1}", tableName, boolString), dbConnection, dbTransaction, null); 79 | } 80 | 81 | internal static bool TableExists(string tableName, DbConnection dbConnection, DbTransaction dbTransaction) 82 | { 83 | return Convert.ToBoolean(ExecuteScalar(string.Format("SELECT CASE WHEN OBJECT_ID(N'{0}', N'U') IS NOT NULL THEN 1 ELSE 0 END", tableName), 84 | dbConnection, dbTransaction, null)); 85 | } 86 | 87 | internal static object GetDBValue(object value) 88 | { 89 | return value == DBNull.Value ? null : value; 90 | } 91 | } 92 | } --------------------------------------------------------------------------------